From b04306991210f96f6015ef04afa7cda44655b48c Mon Sep 17 00:00:00 2001 From: Jamie Cook Date: Thu, 1 Feb 2024 15:44:04 +1000 Subject: [PATCH 1/8] one big squashed commit --- .gitignore | 2 + .vscode/settings.json | 13 + aequilibrae/parameters.yml | 72 + .../project/network/download_parquet.ipynb | 1608 +++ aequilibrae/project/network/messing_around.py | 797 ++ .../project/network/more downloads.ipynb | 867 ++ aequilibrae/project/network/network.py | 92 + aequilibrae/project/network/ovm_builder.py | 437 + aequilibrae/project/network/ovm_downloader.py | 235 + .../examples/creating_models/from_osm.py | 2 + .../examples/creating_models/from_ovm.py | 72 + .../source/examples/creating_models/ovm.ipynb | 10010 ++++++++++++++++ requirements.txt | 4 +- .../aequilibrae/paths/Reg_Spiess_3_Vols.ipynb | 433 + .../project/ovm/setup_test_data.ipynb | 157 + .../project/ovm/test_ovm_downloader.py | 61 + .../project/ovm/test_ovm_processor.py | 226 + ...lie_beach_transportation_connector.parquet | Bin 0 -> 57352 bytes ...irlie_beach_transportation_segment.parquet | Bin 0 -> 82913 bytes .../type=segment/ovm.parquet | Bin 0 -> 51967 bytes 20 files changed, 15087 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 aequilibrae/project/network/download_parquet.ipynb create mode 100644 aequilibrae/project/network/messing_around.py create mode 100644 aequilibrae/project/network/more downloads.ipynb create mode 100644 aequilibrae/project/network/ovm_builder.py create mode 100644 aequilibrae/project/network/ovm_downloader.py create mode 100644 docs/source/examples/creating_models/from_ovm.py create mode 100644 docs/source/examples/creating_models/ovm.ipynb create mode 100644 tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb create mode 100644 tests/aequilibrae/project/ovm/setup_test_data.ipynb create mode 100644 tests/aequilibrae/project/ovm/test_ovm_downloader.py create mode 100644 tests/aequilibrae/project/ovm/test_ovm_processor.py create mode 100644 tests/data/overture/theme=transportation/type=connector/airlie_beach_transportation_connector.parquet create mode 100644 tests/data/overture/theme=transportation/type=segment/airlie_beach_transportation_segment.parquet create mode 100755 tests/data/ovm/type=transportation/type=segment/ovm.parquet diff --git a/.gitignore b/.gitignore index d2631e985..a421a2b63 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ build/ coverage.xml #### End snippet +tests/data/overture/theme=transportation/type=connector/transportation_data_connector.parquet +tests/data/overture/theme=transportation/type=segment/transportation_data_segment.parquet diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..dce22a3bc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "python.testing.pytestArgs": [], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "julia.environmentPath": "c:\\Users\\penny\\git\\Aequilibrae", + "python.testing.unittestArgs": [ + "-v", + "-s", + "./tests", + "-p", + "test*.py" + ] +} \ No newline at end of file diff --git a/aequilibrae/parameters.yml b/aequilibrae/parameters.yml index d7fe1be3e..6d48e2f51 100644 --- a/aequilibrae/parameters.yml +++ b/aequilibrae/parameters.yml @@ -230,6 +230,78 @@ network: mode_filter: pedestrian: 'no' unknown_tags: true + ovm: + all_link_types: + - bridleway + - cycleway + - driveway + - footway + - livingStreet + - motorway + - parkingAisle + - pedestrian + - primary + - residential + - secondary + - steps + - tertiary + - track + - trunk + - unclassified + - unknown + modes: + bicycle: + link_types: + - primary + - secondary + - tertiary + - livingStreet + - parkingAisle + - residential + - cycleway + - pedestrian + - track + - unclassified + mode_filter: + bicycle: 'no' + unknown_tags: true + car: + link_types: + - motorway + - trunk + - primary + - secondary + - tertiary + - unclassified + - residential + - livingStreet + - parkingAisle + mode_filter: + motor_vehicle: 'no' + unknown_tags: true + transit: + link_types: + - motorway + - trunk + - primary + - secondary + - tertiary + - unclassified + - residential + - livingStreet + unknown_tags: true + walk: + link_types: + - cycleway + - footway + - steps + - pedestrian + - track + - bridleway + - unclassified + mode_filter: + pedestrian: 'no' + unknown_tags: true gmns: critical_dist: 2 node: diff --git a/aequilibrae/project/network/download_parquet.ipynb b/aequilibrae/project/network/download_parquet.ipynb new file mode 100644 index 000000000..d87a55808 --- /dev/null +++ b/aequilibrae/project/network/download_parquet.ipynb @@ -0,0 +1,1608 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import time\n", + "import re\n", + "from pathlib import Path\n", + "\n", + "import requests\n", + "from aequilibrae.parameters import Parameters\n", + "from aequilibrae.context import get_logger\n", + "import gc\n", + "import importlib.util as iutil\n", + "from aequilibrae.utils import WorkerThread\n", + "\n", + "import duckdb\n", + "import geopandas as gpd\n", + "import subprocess\n", + "import os\n", + "from typing import Union" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def initialise_duckdb_spatial():\n", + " conn = duckdb.connect()\n", + " c = conn.cursor()\n", + "\n", + " c.execute(\n", + " \"\"\"INSTALL spatial; \n", + " INSTALL httpfs;\n", + " INSTALL parquet;\n", + " \"\"\"\n", + " )\n", + " c.execute(\n", + " \"\"\"LOAD spatial;\n", + " LOAD parquet;\n", + " SET s3_region='us-west-2';\n", + " \"\"\"\n", + " )\n", + " return c\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Count
0281
\n", + "
" + ], + "text/plain": [ + " Count\n", + "0 281" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_parquet.zstd.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " COPY(\n", + " SELECT *\n", + " FROM read_parquet('{pth}/*', union_by_name=True)\n", + " WHERE bbox.minx > '{airlie_bbox[0]}'\n", + " AND bbox.maxx < '{airlie_bbox[2]}'\n", + " AND bbox.miny > '{airlie_bbox[1]}'\n", + " AND bbox.maxy < '{airlie_bbox[3]}')\n", + " TO '{pth2}'\n", + " (FORMAT 'parquet', COMPRESSION 'ZSTD')\n", + "\"\"\"\n", + "c = initialise_duckdb_spatial()\n", + "g = c.execute(sql)\n", + "g \n", + "g.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation.zstd.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " COPY(\n", + " SELECT \n", + " ST_GeomFromWKB(geometry) AS geom,\n", + " *\n", + " FROM read_parquet('{pth}/*', union_by_name=True, hive_partitioning=1)\n", + " WHERE bbox.minx > '{airlie_bbox[0]}'\n", + " AND bbox.maxx < '{airlie_bbox[2]}'\n", + " AND bbox.miny > '{airlie_bbox[1]}'\n", + " AND bbox.maxy < '{airlie_bbox[3]}')\n", + " TO '{pth2}'\n", + " WITH (FORMAT 'parquet', CODEC 'ZSTD')\n", + "\"\"\"\n", + "c = initialise_duckdb_spatial()\n", + "g2 = c.execute(sql)\n", + "g2\n", + "g2.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "g2.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "Query interrupted", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: ", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[6], line 18\u001b[0m\n\u001b[0;32m 5\u001b[0m sql \u001b[38;5;241m=\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\"\"\u001b[39m\n\u001b[0;32m 6\u001b[0m \u001b[38;5;124m COPY(\u001b[39m\n\u001b[0;32m 7\u001b[0m \u001b[38;5;124m SELECT \u001b[39m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 15\u001b[0m \u001b[38;5;124m WITH (FORMAT \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mparquet\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m, CODEC \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mZSTD\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m)\u001b[39m\n\u001b[0;32m 16\u001b[0m \u001b[38;5;124m\"\"\"\u001b[39m\n\u001b[0;32m 17\u001b[0m c \u001b[38;5;241m=\u001b[39m initialise_duckdb_spatial()\n\u001b[1;32m---> 18\u001b[0m g2 \u001b[38;5;241m=\u001b[39m \u001b[43mc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexecute\u001b[49m\u001b[43m(\u001b[49m\u001b[43msql\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 19\u001b[0m g2\n\u001b[0;32m 20\u001b[0m g2\u001b[38;5;241m.\u001b[39mdf()\n", + "\u001b[1;31mRuntimeError\u001b[0m: Query interrupted" + ] + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation3.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " COPY(\n", + " SELECT \n", + " *\n", + " FROM read_parquet('{pth}/*', union_by_name=True, hive_partitioning=1)\n", + " WHERE bbox.minx > '{airlie_bbox[0]}'\n", + " AND bbox.maxx < '{airlie_bbox[2]}'\n", + " AND bbox.miny > '{airlie_bbox[1]}'\n", + " AND bbox.maxy < '{airlie_bbox[3]}')\n", + " TO '{pth2}'\n", + " WITH (FORMAT 'parquet', CODEC 'ZSTD')\n", + "\"\"\"\n", + "c = initialise_duckdb_spatial()\n", + "g2 = c.execute(sql)\n", + "g2\n", + "g2.df()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Count
088731
\n", + "
" + ], + "text/plain": [ + " Count\n", + "0 88731" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_parquet.zstd.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " COPY(\n", + " SELECT *\n", + " FROM read_parquet('{pth}', union_by_name=True))\n", + " TO '{pth2}'\n", + " (FORMAT 'parquet', COMPRESSION 'ZSTD')\n", + "\"\"\"\n", + "initialise_duckdb_spatial().execute(sql).df()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
geometry
0[0, 0, 0, 0, 2, 0, 0, 0, 2, 64, 97, 27, 247, 1...
\n", + "
" + ], + "text/plain": [ + " geometry\n", + "0 [0, 0, 0, 0, 2, 0, 0, 0, 2, 64, 97, 27, 247, 1..." + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_parquet.zstd.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " \n", + " SELECT geometry\n", + " FROM read_parquet('{pth}', union_by_name=True)\n", + "\"\"\"\n", + "initialise_duckdb_spatial().execute(sql).df()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LINESTRING (136.879572 37.23146, 136.880325 37.231763, 136.880494 37.231853, 136.880677 37.231988, 136.880733 37.232039, 136.880768 37.232095, 136.880809 37.232265, 136.880852 37.232343, 136.880929 37.232417, 136.881007 37.232456, 136.8812762 37.2325315, 136.881387 37.232569, 136.881471 37.232619, 136.881745 37.232873, 136.881992 37.23307, 136.8821814 37.2332027, 136.8822227 37.233222, 136.8828104 37.233415, 136.883195 37.233544, 136.8833539 37.2336068)\n" + ] + } + ], + "source": [ + "from shapely.geometry import LineString\n", + "import shapely.wkb as wkb\n", + "\n", + "# Assuming 'serialized_geometry' is the given serialized format\n", + "serialized_geometry = bytearray(b'\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x14@a\\x1c%t-\\xcfF@B\\x9d\\xa0{5*\\x84@a\\x1c+\\x9fU\\x9b=@B\\x9d\\xaah\\xf4\\xb6 @a\\x1c-\\x01\\xc0\\xca`@B\\x9d\\xad[\\xee=`@a\\x1c.\\x81\\x88*\\xdc@B\\x9d\\xb1\\xc8d\\x88@@a\\x1c.\\xf6\\xf8\\xf0A@B\\x9d\\xb3t62\\xc2@a\\x1c/@_k\\xa0@B\\x9d\\xb5I\\xf9HV@a\\x1c/\\x96[ \\xb8@B\\x9d\\xba\\xdc\\t\\x80\\xb2@a\\x1c/\\xf0\\x88\\x93\\xb8@B\\x9d\\xbdjY:.@a\\x1c0\\x92\\x03\\xa3#@B\\x9d\\xbf\\xd7\\x1b\\x04h@a\\x1c15\\x97\\x91\\x82@B\\x9d\\xc1\\x1eB\\xe1&@a\\x1c3j%7\\xc8@B\\x9d\\xc3\\x97\\x99\\xe5\\x19@a\\x1c4R\\x82\\x83\\xd3@B\\x9d\\xc4\\xd2,\\x88\\x1e@a\\x1c5\\x02\\xab\\xab\\xeb@B\\x9d\\xc6u\\x9a\\xb6\\xd0@a\\x1c7AJM+@B\\x9d\\xce\\xc8O\\x8f\\x8a@a\\x1c9GIj\\xad@B\\x9d\\xd5<\\xdd\\xd6\\xe0@a\\x1c:\\xd4|\\xc4w@B\\x9d\\xd9\\x96\\x08\\xeba@a\\x1c;+\\x19\\x89>@B\\x9d\\xda7\\xefZ\\x96@a\\x1c?\\xfb\\x98\\x923@B\\x9d\\xe0\\x8a\\xef\\xb2\\xab@a\\x1cC\")\\x1f\\xb4@B\\x9d\\xe4\\xc5\\x11\\x16\\xa9@a\\x1cDoe\\xe9i@B\\x9d\\xe6\\xd3\\xdf\\x0f\\xc5')\n", + "\n", + "# Convert to bytes\n", + "serialized_geometry_bytes = bytes(serialized_geometry)\n", + "\n", + "# Use shapely.wkb.loads to convert the serialized geometry to a LineString\n", + "line_string = wkb.loads(serialized_geometry_bytes)\n", + "\n", + "# Print the LineString\n", + "print(line_string)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_parquet.zstd.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " \n", + " SELECT geometry\n", + " FROM read_parquet('{pth}', union_by_name=True)\n", + "\"\"\"\n", + "g = initialise_duckdb_spatial().execute(sql).df()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idgeometrybboxsubTypelocalityTypenamescontextIdadminLevelisoCountryCodeAlpha2isoSubCountryCode...socialsemailsphonesbrandaddressessourceTagswikidatasurfaceisSaltisIntermittent
08a2e712b282ffff-17DFF24660C0674BLINESTRING (136.87391 37.21506, 136.87418 37.2...{'minx': 136.8739104, 'maxx': 136.8741804, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
1882e7174d3fffff-13FF4449D72183C2LINESTRING (136.87957 37.23146, 136.88032 37.2...{'minx': 136.879572, 'maxx': 136.8833539, 'min...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
2892e7174d33ffff-17FE1106542131D2LINESTRING (136.88655 37.22834, 136.88672 37.2...{'minx': 136.886546, 'maxx': 136.886733, 'miny...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
3872e71749ffffff-13CDE03A24646A31LINESTRING (136.86781 37.24933, 136.86757 37.2...{'minx': 136.865495, 'maxx': 136.867811, 'miny...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
4872e7174dffffff-13FFCFC79C7F8D41LINESTRING (136.88335 37.23361, 136.88340 37.2...{'minx': 136.8833539, 'maxx': 136.8888252, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
..................................................................
887268804ebaf6dfffff-157B56F2364053C0LINESTRING (171.81161 69.73536, 171.81055 69.7...{'minx': 171.8036842, 'maxx': 171.8116128, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
887278804ebaf61fffff-159FD531CC11F67BLINESTRING (171.83673 69.73090, 171.83653 69.7...{'minx': 171.8275881, 'maxx': 171.836729, 'min...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
887288704ebaf6ffffff-15BFEADF6048761DLINESTRING (171.82734 69.72528, 171.82774 69.7...{'minx': 171.8273413, 'maxx': 171.8530772, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
88729860db679fffffff-17EDF07478DAA881LINESTRING (173.78101 68.98210, 173.77863 68.9...{'minx': 173.7595158, 'maxx': 173.7810087, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
88730850db6c7fffffff-17ACF6D5C1797FD5LINESTRING (173.54518 69.69184, 173.54693 69.6...{'minx': 173.5451782, 'maxx': 173.6200343, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
\n", + "

88731 rows × 37 columns

\n", + "
" + ], + "text/plain": [ + " id \\\n", + "0 8a2e712b282ffff-17DFF24660C0674B \n", + "1 882e7174d3fffff-13FF4449D72183C2 \n", + "2 892e7174d33ffff-17FE1106542131D2 \n", + "3 872e71749ffffff-13CDE03A24646A31 \n", + "4 872e7174dffffff-13FFCFC79C7F8D41 \n", + "... ... \n", + "88726 8804ebaf6dfffff-157B56F2364053C0 \n", + "88727 8804ebaf61fffff-159FD531CC11F67B \n", + "88728 8704ebaf6ffffff-15BFEADF6048761D \n", + "88729 860db679fffffff-17EDF07478DAA881 \n", + "88730 850db6c7fffffff-17ACF6D5C1797FD5 \n", + "\n", + " geometry \\\n", + "0 LINESTRING (136.87391 37.21506, 136.87418 37.2... \n", + "1 LINESTRING (136.87957 37.23146, 136.88032 37.2... \n", + "2 LINESTRING (136.88655 37.22834, 136.88672 37.2... \n", + "3 LINESTRING (136.86781 37.24933, 136.86757 37.2... \n", + "4 LINESTRING (136.88335 37.23361, 136.88340 37.2... \n", + "... ... \n", + "88726 LINESTRING (171.81161 69.73536, 171.81055 69.7... \n", + "88727 LINESTRING (171.83673 69.73090, 171.83653 69.7... \n", + "88728 LINESTRING (171.82734 69.72528, 171.82774 69.7... \n", + "88729 LINESTRING (173.78101 68.98210, 173.77863 68.9... \n", + "88730 LINESTRING (173.54518 69.69184, 173.54693 69.6... \n", + "\n", + " bbox subType localityType \\\n", + "0 {'minx': 136.8739104, 'maxx': 136.8741804, 'mi... road None \n", + "1 {'minx': 136.879572, 'maxx': 136.8833539, 'min... road None \n", + "2 {'minx': 136.886546, 'maxx': 136.886733, 'miny... road None \n", + "3 {'minx': 136.865495, 'maxx': 136.867811, 'miny... road None \n", + "4 {'minx': 136.8833539, 'maxx': 136.8888252, 'mi... road None \n", + "... ... ... ... \n", + "88726 {'minx': 171.8036842, 'maxx': 171.8116128, 'mi... road None \n", + "88727 {'minx': 171.8275881, 'maxx': 171.836729, 'min... road None \n", + "88728 {'minx': 171.8273413, 'maxx': 171.8530772, 'mi... road None \n", + "88729 {'minx': 173.7595158, 'maxx': 173.7810087, 'mi... road None \n", + "88730 {'minx': 173.5451782, 'maxx': 173.6200343, 'mi... road None \n", + "\n", + " names contextId adminLevel isoCountryCodeAlpha2 isoSubCountryCode ... \\\n", + "0 None None NaN None None ... \n", + "1 None None NaN None None ... \n", + "2 None None NaN None None ... \n", + "3 None None NaN None None ... \n", + "4 None None NaN None None ... \n", + "... ... ... ... ... ... ... \n", + "88726 None None NaN None None ... \n", + "88727 None None NaN None None ... \n", + "88728 None None NaN None None ... \n", + "88729 None None NaN None None ... \n", + "88730 None None NaN None None ... \n", + "\n", + " socials emails phones brand addresses sourceTags wikidata surface \\\n", + "0 None None None None None None None None \n", + "1 None None None None None None None None \n", + "2 None None None None None None None None \n", + "3 None None None None None None None None \n", + "4 None None None None None None None None \n", + "... ... ... ... ... ... ... ... ... \n", + "88726 None None None None None None None None \n", + "88727 None None None None None None None None \n", + "88728 None None None None None None None None \n", + "88729 None None None None None None None None \n", + "88730 None None None None None None None None \n", + "\n", + " isSalt isIntermittent \n", + "0 None None \n", + "1 None None \n", + "2 None None \n", + "3 None None \n", + "4 None None \n", + "... ... ... \n", + "88726 None None \n", + "88727 None None \n", + "88728 None None \n", + "88729 None None \n", + "88730 None None \n", + "\n", + "[88731 rows x 37 columns]" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "gdf = gpd.read_parquet(pth)\n", + "gdf" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 LINESTRING (-176.53736 -43.88630, -176.53686 -...\n", + "Name: geometry, dtype: geometry" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gdf[: 1][\"geometry\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idgeometrybboxsubTypelocalityTypenamescontextIdadminLevelisoCountryCodeAlpha2isoSubCountryCode...socialsemailsphonesbrandaddressessourceTagswikidatasurfaceisSaltisIntermittent
1882e7174d3fffff-13FF4449D72183C2LINESTRING (136.87957 37.23146, 136.88032 37.2...{'minx': 136.879572, 'maxx': 136.8833539, 'min...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
2892e7174d33ffff-17FE1106542131D2LINESTRING (136.88655 37.22834, 136.88672 37.2...{'minx': 136.886546, 'maxx': 136.886733, 'miny...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
3872e71749ffffff-13CDE03A24646A31LINESTRING (136.86781 37.24933, 136.86757 37.2...{'minx': 136.865495, 'maxx': 136.867811, 'miny...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
4872e7174dffffff-13FFCFC79C7F8D41LINESTRING (136.88335 37.23361, 136.88340 37.2...{'minx': 136.8833539, 'maxx': 136.8888252, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
\n", + "

4 rows × 37 columns

\n", + "
" + ], + "text/plain": [ + " id \\\n", + "1 882e7174d3fffff-13FF4449D72183C2 \n", + "2 892e7174d33ffff-17FE1106542131D2 \n", + "3 872e71749ffffff-13CDE03A24646A31 \n", + "4 872e7174dffffff-13FFCFC79C7F8D41 \n", + "\n", + " geometry \\\n", + "1 LINESTRING (136.87957 37.23146, 136.88032 37.2... \n", + "2 LINESTRING (136.88655 37.22834, 136.88672 37.2... \n", + "3 LINESTRING (136.86781 37.24933, 136.86757 37.2... \n", + "4 LINESTRING (136.88335 37.23361, 136.88340 37.2... \n", + "\n", + " bbox subType localityType \\\n", + "1 {'minx': 136.879572, 'maxx': 136.8833539, 'min... road None \n", + "2 {'minx': 136.886546, 'maxx': 136.886733, 'miny... road None \n", + "3 {'minx': 136.865495, 'maxx': 136.867811, 'miny... road None \n", + "4 {'minx': 136.8833539, 'maxx': 136.8888252, 'mi... road None \n", + "\n", + " names contextId adminLevel isoCountryCodeAlpha2 isoSubCountryCode ... \\\n", + "1 None None NaN None None ... \n", + "2 None None NaN None None ... \n", + "3 None None NaN None None ... \n", + "4 None None NaN None None ... \n", + "\n", + " socials emails phones brand addresses sourceTags wikidata surface isSalt \\\n", + "1 None None None None None None None None None \n", + "2 None None None None None None None None None \n", + "3 None None None None None None None None None \n", + "4 None None None None None None None None None \n", + "\n", + " isIntermittent \n", + "1 None \n", + "2 None \n", + "3 None \n", + "4 None \n", + "\n", + "[4 rows x 37 columns]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test = gdf[1:5]\n", + "test" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_parquet.zstd.parquet'\n", + "test.to_parquet(pth,compression='zstd')" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import geopandas as gpd\n", + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_parquet.zstd.parquet'\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ]\n", + "\n", + "sql = f\"\"\"\n", + " \n", + " SELECT *\n", + " FROM read_parquet('{pth}', union_by_name=True)\n", + "\"\"\"\n", + "initialise_duckdb_spatial().execute(sql)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import geopandas as gpd\n", + "\n", + "# Set the path to the 'parquet_data' directory\n", + "pth = r'E:\\theme=transportation\\type=segment'\n", + "pth2 = r'C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\\transportation_data_files.zstd.parquet'\n", + "\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621]\n", + "\n", + "\n", + "# Loop through all the parquet files in the 'parquet_data' directory\n", + "for file in os.listdir(pth.replace(\"\\\\\", \"/\")):\n", + " if file.endswith('.parquet'):\n", + " # Read the parquet file into a geopandas GeoDataFrame\n", + " gdf = gpd.read_parquet(os.path.join(pth, file))\n", + "\n", + " # Filter the data using the WHERE command\n", + " filtered_gdf = gdf[gdf['bbox'].apply(lambda bbox: airlie_bbox[0] <= bbox['minx'] <= airlie_bbox[2] and\n", + " airlie_bbox[0] <= bbox['maxx'] <= airlie_bbox[2] and\n", + " airlie_bbox[1] <= bbox['miny'] <= airlie_bbox[3] and\n", + " airlie_bbox[1] <= bbox['maxy'] <= airlie_bbox[3])]\n", + "\n", + " # Save the filtered data to a new parquet file\n", + " filtered_gdf.to_parquet(pth2, compression='zstd')" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'minx': -176.236521,\n", + " 'maxx': -176.236365,\n", + " 'miny': -44.2445032,\n", + " 'maxy': -44.2442786}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# for i in 1:gdf.length:\n", + "gdf['bbox'][1]" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idgeometrybboxsubTypelocalityTypenamescontextIdadminLevelisoCountryCodeAlpha2isoSubCountryCode...socialsemailsphonesbrandaddressessourceTagswikidatasurfaceisSaltisIntermittent
08a2e712b282ffff-17DFF24660C0674BLINESTRING (136.87391 37.21506, 136.87418 37.2...{'minx': 136.8739104, 'maxx': 136.8741804, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
1882e7174d3fffff-13FF4449D72183C2LINESTRING (136.87957 37.23146, 136.88032 37.2...{'minx': 136.879572, 'maxx': 136.8833539, 'min...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
2892e7174d33ffff-17FE1106542131D2LINESTRING (136.88655 37.22834, 136.88672 37.2...{'minx': 136.886546, 'maxx': 136.886733, 'miny...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
3872e71749ffffff-13CDE03A24646A31LINESTRING (136.86781 37.24933, 136.86757 37.2...{'minx': 136.865495, 'maxx': 136.867811, 'miny...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
4872e7174dffffff-13FFCFC79C7F8D41LINESTRING (136.88335 37.23361, 136.88340 37.2...{'minx': 136.8833539, 'maxx': 136.8888252, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
..................................................................
887268804ebaf6dfffff-157B56F2364053C0LINESTRING (171.81161 69.73536, 171.81055 69.7...{'minx': 171.8036842, 'maxx': 171.8116128, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
887278804ebaf61fffff-159FD531CC11F67BLINESTRING (171.83673 69.73090, 171.83653 69.7...{'minx': 171.8275881, 'maxx': 171.836729, 'min...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
887288704ebaf6ffffff-15BFEADF6048761DLINESTRING (171.82734 69.72528, 171.82774 69.7...{'minx': 171.8273413, 'maxx': 171.8530772, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
88729860db679fffffff-17EDF07478DAA881LINESTRING (173.78101 68.98210, 173.77863 68.9...{'minx': 173.7595158, 'maxx': 173.7810087, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
88730850db6c7fffffff-17ACF6D5C1797FD5LINESTRING (173.54518 69.69184, 173.54693 69.6...{'minx': 173.5451782, 'maxx': 173.6200343, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
\n", + "

88731 rows × 37 columns

\n", + "
" + ], + "text/plain": [ + " id \\\n", + "0 8a2e712b282ffff-17DFF24660C0674B \n", + "1 882e7174d3fffff-13FF4449D72183C2 \n", + "2 892e7174d33ffff-17FE1106542131D2 \n", + "3 872e71749ffffff-13CDE03A24646A31 \n", + "4 872e7174dffffff-13FFCFC79C7F8D41 \n", + "... ... \n", + "88726 8804ebaf6dfffff-157B56F2364053C0 \n", + "88727 8804ebaf61fffff-159FD531CC11F67B \n", + "88728 8704ebaf6ffffff-15BFEADF6048761D \n", + "88729 860db679fffffff-17EDF07478DAA881 \n", + "88730 850db6c7fffffff-17ACF6D5C1797FD5 \n", + "\n", + " geometry \\\n", + "0 LINESTRING (136.87391 37.21506, 136.87418 37.2... \n", + "1 LINESTRING (136.87957 37.23146, 136.88032 37.2... \n", + "2 LINESTRING (136.88655 37.22834, 136.88672 37.2... \n", + "3 LINESTRING (136.86781 37.24933, 136.86757 37.2... \n", + "4 LINESTRING (136.88335 37.23361, 136.88340 37.2... \n", + "... ... \n", + "88726 LINESTRING (171.81161 69.73536, 171.81055 69.7... \n", + "88727 LINESTRING (171.83673 69.73090, 171.83653 69.7... \n", + "88728 LINESTRING (171.82734 69.72528, 171.82774 69.7... \n", + "88729 LINESTRING (173.78101 68.98210, 173.77863 68.9... \n", + "88730 LINESTRING (173.54518 69.69184, 173.54693 69.6... \n", + "\n", + " bbox subType localityType \\\n", + "0 {'minx': 136.8739104, 'maxx': 136.8741804, 'mi... road None \n", + "1 {'minx': 136.879572, 'maxx': 136.8833539, 'min... road None \n", + "2 {'minx': 136.886546, 'maxx': 136.886733, 'miny... road None \n", + "3 {'minx': 136.865495, 'maxx': 136.867811, 'miny... road None \n", + "4 {'minx': 136.8833539, 'maxx': 136.8888252, 'mi... road None \n", + "... ... ... ... \n", + "88726 {'minx': 171.8036842, 'maxx': 171.8116128, 'mi... road None \n", + "88727 {'minx': 171.8275881, 'maxx': 171.836729, 'min... road None \n", + "88728 {'minx': 171.8273413, 'maxx': 171.8530772, 'mi... road None \n", + "88729 {'minx': 173.7595158, 'maxx': 173.7810087, 'mi... road None \n", + "88730 {'minx': 173.5451782, 'maxx': 173.6200343, 'mi... road None \n", + "\n", + " names contextId adminLevel isoCountryCodeAlpha2 isoSubCountryCode ... \\\n", + "0 None None NaN None None ... \n", + "1 None None NaN None None ... \n", + "2 None None NaN None None ... \n", + "3 None None NaN None None ... \n", + "4 None None NaN None None ... \n", + "... ... ... ... ... ... ... \n", + "88726 None None NaN None None ... \n", + "88727 None None NaN None None ... \n", + "88728 None None NaN None None ... \n", + "88729 None None NaN None None ... \n", + "88730 None None NaN None None ... \n", + "\n", + " socials emails phones brand addresses sourceTags wikidata surface \\\n", + "0 None None None None None None None None \n", + "1 None None None None None None None None \n", + "2 None None None None None None None None \n", + "3 None None None None None None None None \n", + "4 None None None None None None None None \n", + "... ... ... ... ... ... ... ... ... \n", + "88726 None None None None None None None None \n", + "88727 None None None None None None None None \n", + "88728 None None None None None None None None \n", + "88729 None None None None None None None None \n", + "88730 None None None None None None None None \n", + "\n", + " isSalt isIntermittent \n", + "0 None None \n", + "1 None None \n", + "2 None None \n", + "3 None None \n", + "4 None None \n", + "... ... ... \n", + "88726 None None \n", + "88727 None None \n", + "88728 None None \n", + "88729 None None \n", + "88730 None None \n", + "\n", + "[88731 rows x 37 columns]" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "gdf = gpd.read_parquet(pth)\n", + "gdf" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Empty GeoDataFrame\n", + "Columns: [id, geometry, bbox, subType, localityType, names, contextId, adminLevel, isoCountryCodeAlpha2, isoSubCountryCode, defaultLanguage, drivingSide, version, updateTime, sources, isMaritime, geopolDisplay, localityId, height, numFloors, class, level, connectors, road, categories, confidence, websites, socials, emails, phones, brand, addresses, sourceTags, wikidata, surface, isSalt, isIntermittent]\n", + "Index: []\n", + "\n", + "[0 rows x 37 columns]\n" + ] + } + ], + "source": [ + "# Define the bounding box for the query\n", + "airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621]\n", + "\n", + "# Query for geometries whose bounding boxes intersect with the airlie_bbox\n", + "result = gdf[gdf['geometry'].apply(lambda geom: geom.intersects(airlie_bbox))]\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Empty GeoDataFrame\n", + "Columns: [id, geometry, bbox, subType, localityType, names, contextId, adminLevel, isoCountryCodeAlpha2, isoSubCountryCode, defaultLanguage, drivingSide, version, updateTime, sources, isMaritime, geopolDisplay, localityId, height, numFloors, class, level, connectors, road, categories, confidence, websites, socials, emails, phones, brand, addresses, sourceTags, wikidata, surface, isSalt, isIntermittent]\n", + "Index: []\n", + "\n", + "[0 rows x 37 columns]\n" + ] + } + ], + "source": [ + "from shapely.geometry import box\n", + "\n", + "# Define the bounding box for the query\n", + "airlie_bbox = box(148.7077, -20.2780, 148.7324, -20.2621)\n", + "\n", + "# Convert the bounding box coordinates to a Shapely box geometry\n", + "query_bbox = box(*airlie_bbox.bounds)\n", + "\n", + "# Check if the bounding box in each row intersects with the query_bbox\n", + "result = gdf[gdf['bbox'].apply(lambda bbox: box(bbox['minx'], bbox['miny'], bbox['maxx'], bbox['maxy']).intersects(query_bbox))]\n", + "\n", + "# Print the resulting GeoDataFrame\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aequilibrae/project/network/messing_around.py b/aequilibrae/project/network/messing_around.py new file mode 100644 index 000000000..243ecaa2f --- /dev/null +++ b/aequilibrae/project/network/messing_around.py @@ -0,0 +1,797 @@ +# %% +import importlib.util as iutil +import math +from sqlite3 import Connection as sqlc +from typing import Dict + +import numpy as np +import pandas as pd +import shapely.wkb +import shapely.wkt +from shapely.geometry import Polygon +from shapely.ops import unary_union + +from aequilibrae.context import get_logger +from aequilibrae.parameters import Parameters +from aequilibrae.project.network import OSMDownloader +from aequilibrae.project.network.gmns_builder import GMNSBuilder +from aequilibrae.project.network.gmns_exporter import GMNSExporter +from aequilibrae.project.network.haversine import haversine +from aequilibrae.project.network.link_types import LinkTypes +from aequilibrae.project.network.links import Links +from aequilibrae.project.network.modes import Modes +from aequilibrae.project.network.nodes import Nodes +from aequilibrae.project.network.osm_builder import OSMBuilder +from aequilibrae.project.network.osm_utils.place_getter import placegetter +from aequilibrae.project.project_creation import req_link_flds, req_node_flds, protected_fields +from aequilibrae.utils import WorkerThread + +spec = iutil.find_spec("PyQt5") +pyqt = spec is not None +if pyqt: + from PyQt5.QtCore import pyqtSignal as SIGNAL + + +class Network(WorkerThread): + """ + Network class. Member of an AequilibraE Project + """ + + if pyqt: + netsignal = SIGNAL(object) + + req_link_flds = req_link_flds + req_node_flds = req_node_flds + protected_fields = protected_fields + link_types: LinkTypes = None + + def __init__(self, project) -> None: + from aequilibrae.paths import Graph + + WorkerThread.__init__(self, None) + self.conn = project.conn # type: sqlc + self.source = project.source # type: sqlc + self.graphs = {} # type: Dict[Graph] + self.project = project + self.logger = project.logger + self.modes = Modes(self) + self.link_types = LinkTypes(self) + self.links = Links(self) + self.nodes = Nodes(self) + + def skimmable_fields(self): + """ + Returns a list of all fields that can be skimmed + + :Returns: + :obj:`list`: List of all fields that can be skimmed + """ + curr = self.conn.cursor() + curr.execute("PRAGMA table_info(links);") + field_names = curr.fetchall() + ignore_fields = ["ogc_fid", "geometry"] + self.req_link_flds + + skimmable = [ + "INT", + "INTEGER", + "TINYINT", + "SMALLINT", + "MEDIUMINT", + "BIGINT", + "UNSIGNED BIG INT", + "INT2", + "INT8", + "REAL", + "DOUBLE", + "DOUBLE PRECISION", + "FLOAT", + "DECIMAL", + "NUMERIC", + ] + all_fields = [] + + for f in field_names: + if f[1] in ignore_fields: + continue + for i in skimmable: + if i in f[2].upper(): + all_fields.append(f[1]) + break + + all_fields.append("distance") + real_fields = [] + for f in all_fields: + if f[-2:] == "ab": + if f[:-2] + "ba" in all_fields: + real_fields.append(f[:-3]) + elif f[-3:] != "_ba": + real_fields.append(f) + + return real_fields + + def list_modes(self): + """ + Returns a list of all the modes in this model + + :Returns: + :obj:`list`: List of all modes + """ + curr = self.conn.cursor() + curr.execute("""select mode_id from modes""") + return [x[0] for x in curr.fetchall()] + + def create_from_osm( + self, + west: float = None, + south: float = None, + east: float = None, + north: float = None, + place_name: str = None, + modes=["car", "transit", "bicycle", "walk"], + ) -> None: + """ + Downloads the network from Open-Street Maps + + :Arguments: + **west** (:obj:`float`, Optional): West most coordinate of the download bounding box + + **south** (:obj:`float`, Optional): South most coordinate of the download bounding box + + **east** (:obj:`float`, Optional): East most coordinate of the download bounding box + + **place_name** (:obj:`str`, Optional): If not downloading with East-West-North-South boundingbox, this is + required + + **modes** (:obj:`list`, Optional): List of all modes to be downloaded. Defaults to the modes in the parameter + file + + .. code-block:: python + + >>> from aequilibrae import Project + + >>> p = Project() + >>> p.new("/tmp/new_project") + + # We now choose a different overpass endpoint (say a deployment in your local network) + >>> par = Parameters() + >>> par.parameters['osm']['overpass_endpoint'] = "http://192.168.1.234:5678/api" + + # Because we have our own server, we can set a bigger area for download (in M2) + >>> par.parameters['osm']['max_query_area_size'] = 10000000000 + + # And have no pause between successive queries + >>> par.parameters['osm']['sleeptime'] = 0 + + # Save the parameters to disk + >>> par.write_back() + + # Now we can import the network for any place we want + # p.network.create_from_osm(place_name="my_beautiful_hometown") + + >>> p.close() + """ + + if self.count_links() > 0: + raise FileExistsError("You can only import an OSM network into a brand new model file") + + curr = self.conn.cursor() + curr.execute("""ALTER TABLE links ADD COLUMN osm_id integer""") + curr.execute("""ALTER TABLE nodes ADD COLUMN osm_id integer""") + self.conn.commit() + + if isinstance(modes, (tuple, list)): + modes = list(modes) + elif isinstance(modes, str): + modes = [modes] + else: + raise ValueError("'modes' needs to be string or list/tuple of string") + + if place_name is None: + if min(east, west) < -180 or max(east, west) > 180 or min(north, south) < -90 or max(north, south) > 90: + raise ValueError("Coordinates out of bounds") + bbox = [west, south, east, north] + else: + bbox, report = placegetter(place_name) + west, south, east, north = bbox + if bbox is None: + msg = f'We could not find a reference for place name "{place_name}"' + self.logger.warning(msg) + return + for i in report: + if "PLACE FOUND" in i: + self.logger.info(i) + + # Need to compute the size of the bounding box to not exceed it too much + height = haversine((east + west) / 2, south, (east + west) / 2, north) + width = haversine(east, (north + south) / 2, west, (north + south) / 2) + area = height * width + + par = Parameters().parameters["osm"] + max_query_area_size = par["max_query_area_size"] + + if area < max_query_area_size: + polygons = [bbox] + else: + polygons = [] + parts = math.ceil(area / max_query_area_size) + horizontal = math.ceil(math.sqrt(parts)) + vertical = math.ceil(parts / horizontal) + dx = (east - west) / horizontal + dy = (north - south) / vertical + for i in range(horizontal): + xmin = max(-180, west + i * dx) + xmax = min(180, west + (i + 1) * dx) + for j in range(vertical): + ymin = max(-90, south + j * dy) + ymax = min(90, south + (j + 1) * dy) + box = [xmin, ymin, xmax, ymax] + polygons.append(box) + self.logger.info("Downloading data") + self.downloader = OSMDownloader(polygons, modes, logger=self.logger) + if pyqt: + self.downloader.downloading.connect(self.signal_handler) + + self.downloader.doWork() + + self.logger.info("Building Network") + self.builder = OSMBuilder(self.downloader.json, self.source, project=self.project) + + if pyqt: + self.builder.building.connect(self.signal_handler) + self.builder.doWork() + + self.logger.info("Network built successfully") + + def create_from_gmns( + self, + link_file_path: str, + node_file_path: str, + use_group_path: str = None, + geometry_path: str = None, + srid: int = 4326, + ) -> None: + """ + Creates AequilibraE model from links and nodes in GMNS format. + + :Arguments: + **link_file_path** (:obj:`str`): Path to a links csv file in GMNS format + + **node_file_path** (:obj:`str`): Path to a nodes csv file in GMNS format + + **use_group_path** (:obj:`str`, Optional): Path to a csv table containing groupings of uses. This helps AequilibraE + know when a GMNS use is actually a group of other GMNS uses + + **geometry_path** (:obj:`str`, Optional): Path to a csv file containing geometry information for a line object, if not + specified in the link table + + **srid** (:obj:`int`, Optional): Spatial Reference ID in which the GMNS geometries were created + """ + + gmns_builder = GMNSBuilder(self, link_file_path, node_file_path, use_group_path, geometry_path, srid) + gmns_builder.doWork() + + self.logger.info("Network built successfully") + + def export_to_gmns(self, path: str): + """ + Exports AequilibraE network to csv files in GMNS format. + + :Arguments: + **path** (:obj:`str`): Output folder path. + """ + + gmns_exporter = GMNSExporter(self, path) + gmns_exporter.doWork() + + self.logger.info("Network exported successfully") + + def signal_handler(self, val): + if pyqt: + self.netsignal.emit(val) + + def build_graphs(self, fields: list = None, modes: list = None) -> None: + """Builds graphs for all modes currently available in the model + + When called, it overwrites all graphs previously created and stored in the networks' + dictionary of graphs + + :Arguments: + **fields** (:obj:`list`, optional): When working with very large graphs with large number of fields in the + database, it may be useful to specify which fields to use + **modes** (:obj:`list`, optional): When working with very large graphs with large number of fields in the + database, it may be useful to generate only those we need + + To use the *fields* parameter, a minimalistic option is the following + + .. code-block:: python + + >>> from aequilibrae import Project + + >>> p = Project.from_path("/tmp/test_project") + >>> fields = ['distance'] + >>> p.network.build_graphs(fields, modes = ['c', 'w']) + + """ + from aequilibrae.paths import Graph + + curr = self.conn.cursor() + + if fields is None: + curr.execute("PRAGMA table_info(links);") + field_names = curr.fetchall() + + ignore_fields = ["ogc_fid", "geometry"] + all_fields = [f[1] for f in field_names if f[1] not in ignore_fields] + else: + fields.extend(["link_id", "a_node", "b_node", "direction", "modes"]) + all_fields = list(set(fields)) + + if modes is None: + modes = curr.execute("select mode_id from modes;").fetchall() + modes = [m[0] for m in modes] + elif isinstance(modes, str): + modes = [modes] + + sql = f"select {','.join(all_fields)} from links" + + df = pd.read_sql(sql, self.conn).fillna(value=np.nan) + valid_fields = list(df.select_dtypes(np.number).columns) + ["modes"] + curr.execute("select node_id from nodes where is_centroid=1 order by node_id;") + centroids = np.array([i[0] for i in curr.fetchall()], np.uint32) + + data = df[valid_fields] + for m in modes: + net = pd.DataFrame(data, copy=True) + net.loc[~net.modes.str.contains(m), "b_node"] = net.loc[~net.modes.str.contains(m), "a_node"] + g = Graph() + g.mode = m + g.network = net + if centroids.shape[0]: + g.prepare_graph(centroids) + g.set_blocked_centroid_flows(True) + else: + get_logger().warning("Your graph has no centroids") + self.graphs[m] = g + + def set_time_field(self, time_field: str) -> None: + """ + Set the time field for all graphs built in the model + + :Arguments: + **time_field** (:obj:`str`): Network field with travel time information + """ + for m, g in self.graphs.items(): + if time_field not in list(g.graph.columns): + raise ValueError(f"{time_field} not available. Check if you have NULL values in the database") + g.free_flow_time = time_field + g.set_graph(time_field) + self.graphs[m] = g + + def count_links(self) -> int: + """ + Returns the number of links in the model + + :Returns: + :obj:`int`: Number of links + """ + return self.__count_items("link_id", "links", "link_id>=0") + + def count_centroids(self) -> int: + """ + Returns the number of centroids in the model + + :Returns: + :obj:`int`: Number of centroids + """ + return self.__count_items("node_id", "nodes", "is_centroid=1") + + def count_nodes(self) -> int: + """ + Returns the number of nodes in the model + + :Returns: + :obj:`int`: Number of nodes + """ + return self.__count_items("node_id", "nodes", "node_id>=0") + + def extent(self): + """Queries the extent of the network included in the model + + :Returns: + **model extent** (:obj:`Polygon`): Shapely polygon with the bounding box of the model network. + """ + curr = self.conn.cursor() + curr.execute('Select ST_asBinary(GetLayerExtent("Links"))') + poly = shapely.wkb.loads(curr.fetchone()[0]) + return poly + + def convex_hull(self) -> Polygon: + """Queries the model for the convex hull of the entire network + + :Returns: + **model coverage** (:obj:`Polygon`): Shapely (Multi)polygon of the model network. + """ + curr = self.conn.cursor() + curr.execute('Select ST_asBinary("geometry") from Links where ST_Length("geometry") > 0;') + links = [shapely.wkb.loads(x[0]) for x in curr.fetchall()] + return unary_union(links).convex_hull + + def refresh_connection(self): + """Opens a new database connection to avoid thread conflict""" + self.conn = self.project.connect() + + def __count_items(self, field: str, table: str, condition: str) -> int: + c = self.conn.execute(f"select count({field}) from {table} where {condition};").fetchone()[0] + return c + +# %% +# Imports +from uuid import uuid4 +from tempfile import gettempdir +from os.path import join +from aequilibrae import Project +import folium +# sphinx_gallery_thumbnail_path = 'images/nauru.png' + +# %% +# We create an empty project on an arbitrary folder +fldr = join(gettempdir(), uuid4().hex) +project = Project() +project.new(fldr) + +# %% +project.network.create_from_osm(place_name="Airlie Beach") + + +# %% +links = project.network.links.data +links + +# %% +project.network.nodes.data + +#%% +project.network.count_links() + +# %% +project.network.count_nodes() + +# %% +curr = project.network.conn.cursor() +project.network.conn.commit() + +# %% +modes = Modes(project.network) +modes=["car", "transit", "bicycle", "walk"] +if isinstance(modes, (tuple, list)): + modes = list(modes) +elif isinstance(modes, str): + modes = [modes] +else: + raise ValueError("modes needs to be string or list/tuple of string") +modes + +# %% +par = Parameters().parameters["osm"] +max_query_area_size = par["max_query_area_size"] +par + +# %% +Parameters().__dict__ + +# %% +Parameters().parameters["network"] + +# %% +place_name = "Nauru" + +# %% +north: float = None +east: float = None +south: float = None +west: float = None + +if place_name is None: + if min(east, west) < -180 or max(east, west) > 180 or min(north, south) < -90 or max(north, south) > 90: + raise ValueError("Coordinates out of bounds") + bbox = [west, south, east, north] +else: + bbox, report = placegetter(place_name) + west, south, east, north = bbox + if bbox is None: + msg = f'We could not find a reference for place name "{place_name}"' + project.network.logger.warning(msg) + + for i in report: + if "PLACE FOUND" in i: + project.network.logger.info(i) + +# %% +bbox + +# %% +height = haversine((east + west) / 2, south, (east + west) / 2, north) +width = haversine(east, (north + south) / 2, west, (north + south) / 2) +area = height * width +area + +# %% +area < max_query_area_size + +# %% +polygons = [] +parts = math.ceil(area / max_query_area_size) +horizontal = math.ceil(math.sqrt(parts)) +vertical = math.ceil(parts / horizontal) +dx = (east - west) / horizontal +dy = (north - south) / vertical + +# %% +parts +# %% +horizontal +# %% +vertical +# %% +dx +# %% +dy +# %% +for i in range(horizontal): + xmin = max(-180, west + i * dx) + xmax = min(180, west + (i + 1) * dx) + for j in range(vertical): + ymin = max(-90, south + j * dy) + ymax = min(90, south + (j + 1) * dy) + box = [xmin, ymin, xmax, ymax] + polygons.append(box) + +polygons +# %% +ymax +# %% +ymin +# %% +xmax +# %% +xmin +# %% +project.network.downloader = OSMDownloader(polygons, modes, logger= project.network.logger) +project.network.downloader + +# %% +print(project.network.downloader.json) +print(project.network.downloader.report) +print(polygons) +print(project.network.logger) + +# %% +if pyqt: + project.network.builder.building.connect(project.network.signal_handler) + +# project.network.builder.doWork() +# we will come back to the builder + +# %% +project.network.downloader.doWork() +project.network.downloader.json + +# %% + +# looking through downloader's doWork() +import logging +import time +import re +import requests +from aequilibrae.project.network.osm_utils.osm_params import http_headers, memory +from aequilibrae.parameters import Parameters +from aequilibrae.context import get_logger +import gc +import importlib.util as iutil +from aequilibrae.utils import WorkerThread + +spec = iutil.find_spec("PyQt5") +pyqt = spec is not None +if pyqt: + from PyQt5.QtCore import pyqtSignal + +# %% +infrastructure = 'way["highway"]' +query_template = ( + "{memory}[out:json][timeout:{timeout}];({infrastructure}{filters}({south:.6f},{west:.6f}," + "{north:.6f},{east:.6f});>;);out;" + ) + +# %% +query_template + +# %% +project.__getstate__() + +# %% +WorkerThread.__init__(project.network.downloader, None) +project.network.downloader.logger = get_logger() +project.network.downloader.polygons = polygons +project.network.downloader.filter = project.network.downloader.get_osm_filter(modes) +project.network.downloader.report = [] +project.network.downloader.json = [] +par = Parameters().parameters["osm"] +project.network.downloader.overpass_endpoint = par["overpass_endpoint"] +project.network.downloader.timeout = par["timeout"] +project.network.downloader.sleeptime = par["sleeptime"] + +# %% +project.network.downloader.downloading.emit(["maxValue", len(project.network.downloader.polygons)]) +project.network.downloader.downloading.emit(["Value", 0]) + +# %% +m = "" +if memory > 0: + m = f"[maxsize: {memory}]" +memory +# %% +query_str = query_template.format( + north=north, + south=south, + east=east, + west=west, + infrastructure=infrastructure, + filters=project.network.downloader.filter, + timeout=project.network.downloader.timeout, + memory=m, + ) +query_str +# %% +project.network.downloader.overpass_request(data={"data": query_str}, timeout=project.network.downloader.timeout) + +# %% +json = project.network.downloader.overpass_request(data={"data": query_str}, timeout=project.network.downloader.timeout) +json +# %% +if json["elements"]: + project.network.downloader.json.extend(json["elements"]) +del json +project.network.downloader.json + +# %% +gc.collect() + +# %% +for counter, poly in enumerate(project.network.downloader.polygons): + msg = f"Downloading polygon {counter + 1} of {len(project.network.downloader.polygons)}" + project.network.downloader.logger.debug(msg) + project.network.downloader.downloading.emit(["Value", counter]) + project.network.downloader.downloading.emit(["text", msg]) + west, south, east, north = poly + query_str = query_template.format( + north=north, + south=south, + east=east, + west=west, + infrastructure=infrastructure, + filters=project.network.downloader.filter, + timeout=project.network.downloader.timeout, + memory=m, + ) + json = project.network.downloader.overpass_request(data={"data": query_str}, timeout=project.network.downloader.timeout) + if json["elements"]: + project.network.downloader.json.extend(json["elements"]) + del json + gc.collect() +project.network.downloader.downloading.emit(["Value", len(project.network.downloader.polygons)]) +project.network.downloader.downloading.emit(["FinishedDownloading", 0]) +project.network.downloader.json + +# %% +project.network.logger.info("Downloading data") +project.network.downloader = OSMDownloader(polygons, modes, logger=project.network.logger) +print(project.network.downloader.json) +project.network.downloader.doWork() +project.network.downloader.json + +# %% +project.network.logger.info("Building Network") +project.network.builder = OSMBuilder(project.network.downloader.json, project.network.source, project=project.network.project) +if pyqt: + project.network.builder.building.connect(project.network.signal_handler) + +# project.network.builder.doWork() +# can't access doWork() in this file, so we will go through each step of the function separately here +project.network.builder.nodes + +# %% + +# looking through builder's doWork() +import gc +import importlib.util as iutil +import sqlite3 +import string +from typing import List + +import numpy as np +import pandas as pd + +from aequilibrae.context import get_active_project +from aequilibrae.parameters import Parameters +from aequilibrae.project.network.link_types import LinkTypes +from aequilibrae.utils.spatialite_utils import connect_spatialite +from aequilibrae.project.network.haversine import haversine +from aequilibrae.utils import WorkerThread + +spec = iutil.find_spec("PyQt5") +pyqt = spec is not None +if pyqt: + from PyQt5.QtCore import pyqtSignal + +spec = iutil.find_spec("qgis") +isqgis = spec is not None +if isqgis: + import qgis + +# %% +WorkerThread.__init__(project.network.builder, None) +project.network.builder.project = project or get_active_project() +project.network.builder.logger = project.network.builder.project.logger +project.network.builder.conn = None +project.network.builder.__link_types = None # type: LinkTypes +project.network.builder.report = [] +project.network.builder.__model_link_types = [] +project.network.builder.__model_link_type_ids = [] +project.network.builder.__link_type_quick_reference = {} +project.network.builder.nodes = {} +project.network.builder.node_df = [] +project.network.builder.links = {} +project.network.builder.insert_qry = """INSERT INTO {} ({}, geometry) VALUES({}, GeomFromText(?, 4326))""" +# %% +project.network.builder.conn = connect_spatialite(project.network.builder.path) +print(project.network.builder.conn) +project.network.builder.curr = project.network.builder.conn.cursor() +print(project.network.builder.curr) + +# project.network.builder.__worksetup() +# can't access so will go through each step + +# %% +project.network.builder.__link_types = project.network.builder.project.network.link_types +lts = project.network.builder.__link_types.all_types() +for lt_id, lt in lts.items(): + project.network.builder.__model_link_types.append(lt.link_type) + project.network.builder.__model_link_type_ids.append(lt_id) +# %% +print(project.network.builder.__model_link_types) +print(project.network.builder.__model_link_type_ids) +lts + +# %% + +# back to doWork +node_count = project.network.builder.data_structures() +node_count + +# %% + +# project.network.builder.importing_links(node_count) +# this would have been the next step in doWork but again we don't have access to this function +# in this particual case so we will go step by step +node_ids = {} + +vars = {} +vars["link_id"] = 1 +table = "links" +# fields = project.network.builder.get_link_fields() +# same again no access, so step by step +p = Parameters() +fields = p.parameters["network"]["links"]["fields"] +owf = [list(x.keys())[0] for x in fields["one-way"]] + +twf1 = ["{}_ab".format(list(x.keys())[0]) for x in fields["two-way"]] +twf2 = ["{}_ba".format(list(x.keys())[0]) for x in fields["two-way"]] + +return_get_link_fields = owf + twf1 + twf2 + ["osm_id"] +print(fields) +print(owf) +print(twf1) +print(twf2) +print(return_get_link_fields) + +# %% diff --git a/aequilibrae/project/network/more downloads.ipynb b/aequilibrae/project/network/more downloads.ipynb new file mode 100644 index 000000000..68780c8b8 --- /dev/null +++ b/aequilibrae/project/network/more downloads.ipynb @@ -0,0 +1,867 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import duckdb\n", + "import json\n", + "import geopandas as gpd\n", + "import pandas as pd\n", + "from shapely.geometry import LineString, LinearRing\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def initialise_duckdb_spatial():\n", + " conn = duckdb.connect()\n", + " c = conn.cursor()\n", + "\n", + " c.execute(\n", + " \"\"\"INSTALL spatial; \n", + " INSTALL httpfs;\n", + " INSTALL parquet;\n", + " \"\"\"\n", + " )\n", + " c.execute(\n", + " \"\"\"LOAD spatial;\n", + " LOAD parquet;\n", + " SET s3_region='us-west-2';\n", + " \"\"\"\n", + " )\n", + " return c" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idgeometrybboxsubTypelocalityTypenamescontextIdadminLevelisoCountryCodeAlpha2isoSubCountryCode...socialsemailsphonesbrandaddressessourceTagswikidatasurfaceisSaltisIntermittent
08a2e712b282ffff-17DFF24660C0674BLINESTRING (136.87391 37.21506, 136.87418 37.2...{'minx': 136.8739104, 'maxx': 136.8741804, 'mi...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
1882e7174d3fffff-13FF4449D72183C2LINESTRING (136.87957 37.23146, 136.88032 37.2...{'minx': 136.879572, 'maxx': 136.8833539, 'min...roadNoneNoneNoneNaNNoneNone...NoneNoneNoneNoneNoneNoneNoneNoneNoneNone
\n", + "

2 rows × 37 columns

\n", + "
" + ], + "text/plain": [ + " id \\\n", + "0 8a2e712b282ffff-17DFF24660C0674B \n", + "1 882e7174d3fffff-13FF4449D72183C2 \n", + "\n", + " geometry \\\n", + "0 LINESTRING (136.87391 37.21506, 136.87418 37.2... \n", + "1 LINESTRING (136.87957 37.23146, 136.88032 37.2... \n", + "\n", + " bbox subType localityType \\\n", + "0 {'minx': 136.8739104, 'maxx': 136.8741804, 'mi... road None \n", + "1 {'minx': 136.879572, 'maxx': 136.8833539, 'min... road None \n", + "\n", + " names contextId adminLevel isoCountryCodeAlpha2 isoSubCountryCode ... \\\n", + "0 None None NaN None None ... \n", + "1 None None NaN None None ... \n", + "\n", + " socials emails phones brand addresses sourceTags wikidata surface isSalt \\\n", + "0 None None None None None None None None None \n", + "1 None None None None None None None None None \n", + "\n", + " isIntermittent \n", + "0 None \n", + "1 None \n", + "\n", + "[2 rows x 37 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'E:\\theme=transportation\\type=segment\\part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'\n", + "gdf = gpd.read_parquet(pth)\n", + "\n", + "gdf[0:2]" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "numpy.float64" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pth = r'C:\\Users\\penny\\git\\Aequilibrae\\tests\\data\\overture\\theme=transportation\\type=segment\\transportation_data_segment.parquet'\n", + "df = pd.read_parquet(pth)\n", + "geo = gpd.GeoSeries.from_wkb(df.geometry, crs=4326)\n", + "gdf = gpd.GeoDataFrame(df,geometry=geo)\n", + "gdf['speed'] = gdf['speed'].apply(lambda x: json.loads(x)[0] if x else None)\n", + "\n", + "gdf.to_parquet(r'C:\\Users\\penny\\git\\Aequilibrae\\tests\\data\\overture\\theme=transportation\\type=segment\\transportation_data_segment_airlie_beach.parquet')\n", + "type(gdf['speed'][1])" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 NaN\n", + "1 70.0\n", + "2 NaN\n", + "3 50.0\n", + "4 NaN\n", + " ... \n", + "276 NaN\n", + "277 NaN\n", + "278 40.0\n", + "279 50.0\n", + "280 70.0\n", + "Name: speed, Length: 281, dtype: float64" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# gdf['speed'] = \n", + "gdf['speed'].apply(lambda x: json.loads(x)[0] if x else None)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idbboxa_nodeb_nodelink_typespeedgeometry
08b9d0e12872dfff-167FE27DAE6F0272{'maxx': 148.7165748, 'maxy': -20.2730078, 'mi...8f9d0e12872d085-167FE2B34902B1488f9d0e12872d292-167EE24C8AA265F0secondaryNaNLINESTRING (148.71657 -20.27307, 148.71651 -20...
18b9d0e128cd9fff-163FF6797FC40661{'maxx': 148.7247078, 'maxy': -20.2747175, 'mi...8f9d0e128cd9709-167FF64A37F1BFFB8f9d0e128cd98d6-15FFF68E65613FDFsecondary70.0LINESTRING (148.72460 -20.27472, 148.72465 -20...
28a9d0e1284effff-17BFEE367410456E{'maxx': 148.7216872, 'maxy': -20.269086, 'min...8f9d0e1284ec01d-17BFFF2E889165D38f9d0e1284ec0eb-17BFEF097DAAE5D1unknownNaNLINESTRING (148.72169 -20.26929, 148.72163 -20...
3889d0e1287fffff-147FF893549FBF23{'maxx': 148.7193104, 'maxy': -20.270149, 'min...8f9d0e1287a434a-157EE961030FB9708f9d0e12871415a-173FE7D6F5926937secondary50.0LINESTRING (148.71931 -20.27015, 148.71924 -20...
4869d0e12fffffff-167FF1BC7BC46279{'maxx': 148.7228411, 'maxy': -20.2662583, 'mi...8f9d0e12ab2d0b3-16FEF0F6D47F3E2F8f9d0e1284d3910-157EF1FC540A0515unknownNaNLINESTRING (148.72242 -20.26626, 148.72276 -20...
........................
276899d0e12867ffff-15BEF645DFC7D25C{'maxx': 148.7122258, 'maxy': -20.269956, 'min...8f9d0e128646b30-15BFF8152AF1551F8f9d0e12866a2c9-14FFD4B13E67A6AFunknownNaNLINESTRING (148.71223 -20.27006, 148.71161 -20...
277889d0e1285fffff-177EEF26B077B75F{'maxx': 148.7218181, 'maxy': -20.268822, 'min...8f9d0e1284ee0c5-14BEEF805C9D97A78f9d0e1284eeb00-143FEF7119103FFDresidentialNaNLINESTRING (148.72182 -20.26882, 148.72181 -20...
278869d0e12fffffff-15FFD939CAC49013{'maxx': 148.7132732, 'maxy': -20.2644693, 'mi...8f9d0e12bd2bd89-177EFA66EE6BBDE58f9d0e12bd2869c-173FDA8ACA23C819residential40.0LINESTRING (148.71318 -20.26447, 148.71323 -20...
279869d0e12fffffff-14FEFD6B3C7AEF26{'maxx': 148.7145647, 'maxy': -20.2646887, 'mi...8f9d0e12bd22a6a-16FFDDCAF35E76318f9d0e1286d694d-177FFD1D35C31328residential50.0LINESTRING (148.71456 -20.26469, 148.71429 -20...
280889d0e1285fffff-163EF5CEB5006F3F{'maxx': 148.7245302, 'maxy': -20.2746973, 'mi...8f9d0e12856450c-163EF592716BEB4D8f9d0e128cd9662-167EF61F6866D214secondary70.0LINESTRING (148.72430 -20.27484, 148.72433 -20...
\n", + "

281 rows × 7 columns

\n", + "
" + ], + "text/plain": [ + " id \\\n", + "0 8b9d0e12872dfff-167FE27DAE6F0272 \n", + "1 8b9d0e128cd9fff-163FF6797FC40661 \n", + "2 8a9d0e1284effff-17BFEE367410456E \n", + "3 889d0e1287fffff-147FF893549FBF23 \n", + "4 869d0e12fffffff-167FF1BC7BC46279 \n", + ".. ... \n", + "276 899d0e12867ffff-15BEF645DFC7D25C \n", + "277 889d0e1285fffff-177EEF26B077B75F \n", + "278 869d0e12fffffff-15FFD939CAC49013 \n", + "279 869d0e12fffffff-14FEFD6B3C7AEF26 \n", + "280 889d0e1285fffff-163EF5CEB5006F3F \n", + "\n", + " bbox \\\n", + "0 {'maxx': 148.7165748, 'maxy': -20.2730078, 'mi... \n", + "1 {'maxx': 148.7247078, 'maxy': -20.2747175, 'mi... \n", + "2 {'maxx': 148.7216872, 'maxy': -20.269086, 'min... \n", + "3 {'maxx': 148.7193104, 'maxy': -20.270149, 'min... \n", + "4 {'maxx': 148.7228411, 'maxy': -20.2662583, 'mi... \n", + ".. ... \n", + "276 {'maxx': 148.7122258, 'maxy': -20.269956, 'min... \n", + "277 {'maxx': 148.7218181, 'maxy': -20.268822, 'min... \n", + "278 {'maxx': 148.7132732, 'maxy': -20.2644693, 'mi... \n", + "279 {'maxx': 148.7145647, 'maxy': -20.2646887, 'mi... \n", + "280 {'maxx': 148.7245302, 'maxy': -20.2746973, 'mi... \n", + "\n", + " a_node b_node \\\n", + "0 8f9d0e12872d085-167FE2B34902B148 8f9d0e12872d292-167EE24C8AA265F0 \n", + "1 8f9d0e128cd9709-167FF64A37F1BFFB 8f9d0e128cd98d6-15FFF68E65613FDF \n", + "2 8f9d0e1284ec01d-17BFFF2E889165D3 8f9d0e1284ec0eb-17BFEF097DAAE5D1 \n", + "3 8f9d0e1287a434a-157EE961030FB970 8f9d0e12871415a-173FE7D6F5926937 \n", + "4 8f9d0e12ab2d0b3-16FEF0F6D47F3E2F 8f9d0e1284d3910-157EF1FC540A0515 \n", + ".. ... ... \n", + "276 8f9d0e128646b30-15BFF8152AF1551F 8f9d0e12866a2c9-14FFD4B13E67A6AF \n", + "277 8f9d0e1284ee0c5-14BEEF805C9D97A7 8f9d0e1284eeb00-143FEF7119103FFD \n", + "278 8f9d0e12bd2bd89-177EFA66EE6BBDE5 8f9d0e12bd2869c-173FDA8ACA23C819 \n", + "279 8f9d0e12bd22a6a-16FFDDCAF35E7631 8f9d0e1286d694d-177FFD1D35C31328 \n", + "280 8f9d0e12856450c-163EF592716BEB4D 8f9d0e128cd9662-167EF61F6866D214 \n", + "\n", + " link_type speed geometry \n", + "0 secondary NaN LINESTRING (148.71657 -20.27307, 148.71651 -20... \n", + "1 secondary 70.0 LINESTRING (148.72460 -20.27472, 148.72465 -20... \n", + "2 unknown NaN LINESTRING (148.72169 -20.26929, 148.72163 -20... \n", + "3 secondary 50.0 LINESTRING (148.71931 -20.27015, 148.71924 -20... \n", + "4 unknown NaN LINESTRING (148.72242 -20.26626, 148.72276 -20... \n", + ".. ... ... ... \n", + "276 unknown NaN LINESTRING (148.71223 -20.27006, 148.71161 -20... \n", + "277 residential NaN LINESTRING (148.72182 -20.26882, 148.72181 -20... \n", + "278 residential 40.0 LINESTRING (148.71318 -20.26447, 148.71323 -20... \n", + "279 residential 50.0 LINESTRING (148.71456 -20.26469, 148.71429 -20... \n", + "280 secondary 70.0 LINESTRING (148.72430 -20.27484, 148.72433 -20... \n", + "\n", + "[281 rows x 7 columns]" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gdf" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LINESTRING (0 0, 1 1)\n", + "LINESTRING (1 1, 2 2)\n", + "LINESTRING (0 0, 1 0)\n", + "LINESTRING (1 0, 1 1)\n", + "LINESTRING (1 1, 0 1)\n", + "LINESTRING (0 1, 0 0)\n" + ] + } + ], + "source": [ + "from shapely.geometry import LineString, LinearRing\n", + "\n", + "\n", + "def segments(curve):\n", + " return list(map(LineString, zip(curve.coords[:-1], curve.coords[1:])))\n", + "\n", + "\n", + "line = LineString([(0, 0), (1, 1), (2, 2)])\n", + "ring = LinearRing([(0, 0), (1, 0), (1, 1), (0, 1)])\n", + "\n", + "line_segments = segments(line)\n", + "for segment in line_segments:\n", + " print(segment)\n", + "# LINESTRING (0 0, 1 1)\n", + "# LINESTRING (1 1, 2 2)\n", + "\n", + "ring_segments = segments(ring)\n", + "for segment in ring_segments:\n", + " print(segment)\n", + "# LINESTRING (0 0, 1 0)\n", + "# LINESTRING (1 0, 1 1)\n", + "# LINESTRING (1 1, 0 1)\n", + "# LINESTRING (0 1, 0 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'int' object is not iterable", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[6], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mLineString\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m148\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m20\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\shapely\\geometry\\linestring.py:66\u001b[0m, in \u001b[0;36mLineString.__new__\u001b[1;34m(self, coordinates)\u001b[0m\n\u001b[0;32m 63\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 64\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m [\u001b[38;5;28mfloat\u001b[39m(c) \u001b[38;5;28;01mfor\u001b[39;00m c \u001b[38;5;129;01min\u001b[39;00m o]\n\u001b[1;32m---> 66\u001b[0m coordinates \u001b[38;5;241m=\u001b[39m \u001b[43m[\u001b[49m\u001b[43m_coords\u001b[49m\u001b[43m(\u001b[49m\u001b[43mo\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mo\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcoordinates\u001b[49m\u001b[43m]\u001b[49m\n\u001b[0;32m 68\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(coordinates) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m 69\u001b[0m \u001b[38;5;66;03m# empty geometry\u001b[39;00m\n\u001b[0;32m 70\u001b[0m \u001b[38;5;66;03m# TODO better constructor + should shapely.linestrings handle this?\u001b[39;00m\n\u001b[0;32m 71\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m shapely\u001b[38;5;241m.\u001b[39mfrom_wkt(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mLINESTRING EMPTY\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[1;31mTypeError\u001b[0m: 'int' object is not iterable" + ] + } + ], + "source": [ + "LineString(148 -20)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Name Age City\n", + "0 Alice 50 NEW YORK\n", + "1 Bob 60 SAN FRANCISCO\n", + "2 Charlie 70 LOS ANGELES\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "# Example DataFrame\n", + "data = {'Name': ['Alice', 'Bob', 'Charlie'],\n", + " 'Age': [25, 30, 35],\n", + " 'City': ['New York', 'San Francisco', 'Los Angeles']}\n", + "\n", + "df = pd.DataFrame(data)\n", + "\n", + "# Function to process each row and create a new DataFrame\n", + "def process_row(row):\n", + " name = row['Name']\n", + " age = row['Age']\n", + " city = row['City']\n", + "\n", + " # Your processing logic here\n", + " # For example, create a new DataFrame with processed data\n", + " processed_data = {'Name': [name], 'Age': [age * 2], 'City': [city.upper()]}\n", + " processed_df = pd.DataFrame(processed_data)\n", + "\n", + " return processed_df\n", + "\n", + "# Iterate over rows using iterrows()\n", + "result_dfs = []\n", + "for index, row in df.iterrows():\n", + " processed_df = process_row(row)\n", + " result_dfs.append(processed_df)\n", + "\n", + "# Concatenate the resulting DataFrames into a final DataFrame\n", + "final_result = pd.concat(result_dfs, ignore_index=True)\n", + "\n", + "# Display the final result\n", + "print(final_result)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameAgeCity
0Alice25New York
1Bob30San Francisco
2Charlie35Los Angeles
\n", + "
" + ], + "text/plain": [ + " Name Age City\n", + "0 Alice 25 New York\n", + "1 Bob 30 San Francisco\n", + "2 Charlie 35 Los Angeles" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameAgeCity
0Alice50NEW YORK
1Bob60SAN FRANCISCO
2Charlie70LOS ANGELES
\n", + "
" + ], + "text/plain": [ + " Name Age City\n", + "0 Alice 50 NEW YORK\n", + "1 Bob 60 SAN FRANCISCO\n", + "2 Charlie 70 LOS ANGELES" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "final_result" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameAgeCity
0Bob60SAN FRANCISCO
\n", + "
" + ], + "text/plain": [ + " Name Age City\n", + "0 Bob 60 SAN FRANCISCO" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result_dfs[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Name Age City a_node b_node\n", + "0 Alice 25 New York 1 2\n", + "1 Alice 25 New York 2 3\n", + "2 Bob 30 San Francisco 4 5\n", + "3 Bob 30 San Francisco 5 6\n", + "4 Charlie 35 Los Angeles 7 8\n", + "5 Charlie 35 Los Angeles 8 9\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "# Example DataFrame\n", + "data = {'Name': ['Alice', 'Bob', 'Charlie'],\n", + " 'Age': [25, 30, 35],\n", + " 'City': ['New York', 'San Francisco', 'Los Angeles'],\n", + " 'Connectors': [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}\n", + "\n", + "df = pd.DataFrame(data)\n", + "\n", + "# Function to process each row and create a new DataFrame\n", + "def process_row(row):\n", + " name = row['Name']\n", + " age = row['Age']\n", + " city = row['City']\n", + " connectors = row['Connectors']\n", + "\n", + " # Check if 'Connectors' has more than 2 elements\n", + " if np.size(connectors) > 2:\n", + " # Split the DataFrame into multiple rows\n", + " rows = []\n", + " for i in range(len(connectors) - 1):\n", + " new_row = {'Name': name, 'Age': age, 'City': city, 'a_node': connectors[i], 'b_node': connectors[i + 1]}\n", + " rows.append(new_row)\n", + " processed_df = pd.DataFrame(rows)\n", + " else:\n", + " # For cases where 'Connectors' has 2 or fewer elements\n", + " processed_df = pd.DataFrame({'Name': [name], 'Age': [age], 'City': [city], 'a_node': connectors[0], 'b_node': connectors[-1]})\n", + "\n", + " return processed_df\n", + "\n", + "# Iterate over rows using iterrows()\n", + "result_dfs = []\n", + "for index, row in df.iterrows():\n", + " processed_df = process_row(row)\n", + " result_dfs.append(processed_df)\n", + "\n", + "# Concatenate the resulting DataFrames into a final DataFrame\n", + "final_result = pd.concat(result_dfs, ignore_index=True)\n", + "\n", + "# Display the final result\n", + "print(final_result)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/aequilibrae/project/network/network.py b/aequilibrae/project/network/network.py index 094139545..b4532e079 100644 --- a/aequilibrae/project/network/network.py +++ b/aequilibrae/project/network/network.py @@ -2,6 +2,7 @@ import math from sqlite3 import Connection as sqlc from typing import Dict +from pathlib import Path import numpy as np import pandas as pd @@ -13,6 +14,8 @@ from aequilibrae.context import get_logger from aequilibrae.parameters import Parameters from aequilibrae.project.network import OSMDownloader +from aequilibrae.project.network.ovm_builder import OVMBuilder +from aequilibrae.project.network.ovm_downloader import OVMDownloader from aequilibrae.project.network.gmns_builder import GMNSBuilder from aequilibrae.project.network.gmns_exporter import GMNSExporter from aequilibrae.project.network.haversine import haversine @@ -21,6 +24,7 @@ from aequilibrae.project.network.modes import Modes from aequilibrae.project.network.nodes import Nodes from aequilibrae.project.network.osm_builder import OSMBuilder +# from aequilibrae.project.network.ovm_builder import OVMBuilder from aequilibrae.project.network.osm_utils.place_getter import placegetter from aequilibrae.project.project_creation import req_link_flds, req_node_flds, protected_fields from aequilibrae.utils import WorkerThread @@ -119,6 +123,94 @@ def list_modes(self): curr.execute("""select mode_id from modes""") return [x[0] for x in curr.fetchall()] + def create_from_ovm( + self, + west: float = None, + south: float = None, + east: float = None, + north: float = None, + place_name: str = None, + data_source: Path = None, + output_dir: Path = None, + modes=["car", "transit", "bicycle", "walk"], + ) -> None: + """ + Downloads the network from Open-Street Maps + + :Arguments: + **west** (:obj:`float`, Optional): West most coordinate of the download bounding box + + **south** (:obj:`float`, Optional): South most coordinate of the download bounding box + + **east** (:obj:`float`, Optional): East most coordinate of the download bounding box + + **place_name** (:obj:`str`, Optional): If not downloading with East-West-North-South boundingbox, this is + required + + **modes** (:obj:`list`, Optional): List of all modes to be downloaded. Defaults to the modes in the parameter + file + + .. code-block:: python + + >>> from aequilibrae import Project + + >>> p = Project() + >>> p.new("/tmp/new_project") + + # Save the parameters to disk + >>> par.write_back() + + # Now we can import the network for any place we want + # p.network.create_from_ovm(place_name="my_beautiful_hometown") + + >>> p.close() + """ + + if self.count_links() > 0: + raise FileExistsError("You can only import an OVM network into a brand new model file") + + curr = self.conn.cursor() + curr.execute("""ALTER TABLE links ADD COLUMN ovm_id integer""") + curr.execute("""ALTER TABLE nodes ADD COLUMN ovm_id integer""") + self.conn.commit() + + if isinstance(modes, (tuple, list)): + modes = list(modes) + elif isinstance(modes, str): + modes = [modes] + else: + raise ValueError("'modes' needs to be string or list/tuple of string") + + if place_name is None: + if min(east, west) < -180 or max(east, west) > 180 or min(north, south) < -90 or max(north, south) > 90: + raise ValueError("Coordinates out of bounds") + bbox = [west, south, east, north] + else: + bbox, report = placegetter(place_name) + west, south, east, north = bbox + if bbox is None: + msg = f'We could not find a reference for place name "{place_name}"' + self.logger.warning(msg) + return + for i in report: + if "PLACE FOUND" in i: + self.logger.info(i) + + self.logger.info("Downloading data") + self.downloader = OVMDownloader(modes, self.source, logger=self.logger) + if pyqt: + self.downloader.downloading.connect(self.signal_handler) + segments_gdf, connectors_gdf = self.downloader.downloadTransportation(bbox,data_source,output_dir) + + self.logger.info("Building Network") + self.builder = OVMBuilder(segments_gdf, connectors_gdf, self.source, project=self.project) + + if pyqt: + self.builder.building.connect(self.signal_handler) + + self.builder.doWork(output_dir) + self.logger.info("Network built successfully") + def create_from_osm( self, west: float = None, diff --git a/aequilibrae/project/network/ovm_builder.py b/aequilibrae/project/network/ovm_builder.py new file mode 100644 index 000000000..8a153338a --- /dev/null +++ b/aequilibrae/project/network/ovm_builder.py @@ -0,0 +1,437 @@ +import json +import importlib.util as iutil +import sqlite3 +import logging +from pathlib import Path +import string + +from aequilibrae.context import get_active_project +from aequilibrae.parameters import Parameters +from aequilibrae.project.network.link_types import LinkTypes +from aequilibrae.context import get_logger +import importlib.util as iutil +from aequilibrae.utils.spatialite_utils import connect_spatialite +from aequilibrae.project.network.haversine import haversine +from aequilibrae.utils import WorkerThread + +# from .haversine import haversine +# from ...utils import WorkerThread + +import duckdb +import shapely +import geopandas as gpd +import pandas as pd +import numpy as np +from typing import Union +from shapely.geometry import LineString, Point + +spec = iutil.find_spec("PyQt5") +pyqt = spec is not None +if pyqt: + from PyQt5.QtCore import pyqtSignal + +spec = iutil.find_spec("qgis") +isqgis = spec is not None +if isqgis: + import qgis + + +class OVMBuilder(WorkerThread): + if pyqt: + building = pyqtSignal(object) + + def __init__( + self, + gdf_segments: gpd.GeoDataFrame, + gdf_connectors: gpd.GeoDataFrame, + project_path: Union[str, Path], + logger: logging.Logger = None, + node_start=10000, + project=None, + ) -> None: + WorkerThread.__init__(self, None) + self.project = project or get_active_project() + self.logger = logger or get_logger() + self.node_start = node_start + self.report = [] + self.conn = None + self.GeoDataFrame = [] + self.nodes = {} + self.node_ids = {} + self.links_gdf = gdf_segments + self.nodes_gdf = gdf_connectors + self.__link_types = None # type: LinkTypes + self.__model_link_types = [] + self.__model_link_type_ids = [] + self.__link_type_quick_reference = {} + self.__project_path = Path(project_path) + self.pth = str(self.__project_path).replace("\\", "/") + + def __emit_all(self, *args): + if pyqt: + self.building.emit(*args) + + def doWork(self, output_dir: Path): + self.conn = connect_spatialite(self.pth) + self.curr = self.conn.cursor() + self.__worksetup() + self.formatting(self.links_gdf, self.nodes_gdf, output_dir) + self.__emit_all(["finished_threaded_procedure", 0]) + + def formatting(self, links_gdf: gpd.GeoDataFrame, nodes_gdf: gpd.GeoDataFrame, output_dir: Path): + output_dir = Path(output_dir) + output_file_link = output_dir / f"type=segment" / f"transportation_data_segment.parquet" + output_file_node = output_dir / f"type=connector" / f"transportation_data_connector.parquet" + + links_gdf = links_gdf.copy() + links_gdf["name"] = links_gdf["name"].apply(lambda x: json.loads(x)[0]["value"] if x else None) + + nodes_gdf = nodes_gdf.copy() + nodes_gdf["node_id"] = self.create_node_ids(nodes_gdf) + nodes_gdf["ogc_fid"] = pd.Series(list(range(1, len(nodes_gdf) + 1))) + nodes_gdf["is_centroid"] = 0 + + # Iterate over rows using iterrows() + result_dfs = [self.split_connectors(row) for _, row in links_gdf.iterrows()] + + # Concatenate the resulting DataFrames into a final GeoDataFrame + links_gdf = pd.concat((df.dropna(axis=1, how="all") for df in result_dfs), ignore_index=True) + + # adding neccassary columns for aequilibrea data frame + links_gdf["link_id"] = pd.Series(list(range(1, len(links_gdf) + 1))) + links_gdf["ogc_fid"] = pd.Series(list(range(1, len(links_gdf) + 1))) + links_gdf["geometry"] = [ + self.trim_geometry(self.node_ids, row) + for e, row in links_gdf[["a_node", "b_node", "geometry"]].iterrows() + ] + + distance_list = [] + for i in range(0, len(links_gdf)): + distance = sum( + [ + haversine(x[0], x[1], y[0], y[1]) + for x, y in zip( + list(links_gdf["geometry"][i].coords)[1:], list(links_gdf["geometry"][i].coords)[:-1] + ) + ] + ) + distance_list.append(distance) + links_gdf["distance"] = distance_list + + mode_codes, not_found_tags = self.modes_per_link_type() + links_gdf["modes"] = links_gdf["link_type"].apply(lambda x: mode_codes.get(x, not_found_tags)) + + common_nodes = links_gdf["a_node"].isin(nodes_gdf["node_id"]) + + # Check if any common nodes exist + if common_nodes.any(): + # If common node exist, retrieve the DataFrame of matched rows using boolean indexing + matched_rows = links_gdf[common_nodes] + + # Create the 'link_types' and 'modes' columns for the 'nodes_gdf' DataFrame + nodes_gdf["link_types"] = matched_rows["link_type"] + nodes_gdf["modes"] = matched_rows["modes"] + else: + # No common nodes found + raise ValueError("No common nodes.") + fields = self.get_link_fields() + link_order = fields.copy() + ["geometry"] + + for element in link_order: + if element not in links_gdf: + links_gdf[element] = None + + links_gdf = links_gdf[link_order] + links_gdf.to_parquet(output_file_link) + + # For goemetry to work in the sql + links_gdf = pd.DataFrame(links_gdf) + links_gdf["geometry"] = links_gdf["geometry"].apply(lambda x: x.wkb) + + node_order = ["ogc_fid", "node_id", "is_centroid", "modes", "link_types", "ovm_id", "geometry"] + nodes_gdf = nodes_gdf[node_order] + + nodes_gdf.to_parquet(output_file_node) + + self.__update_table_structure() + field_names = ",".join(fields) + + self.logger.info("Adding network nodes") + self.__emit_all(["text", "Adding network nodes"]) + + node_df = pd.DataFrame(nodes_gdf[["node_id", "is_centroid", "modes", "link_types", "ovm_id"]]) # drop geom and ogc_fid + node_df['x'] = nodes_gdf.geometry.apply(lambda x: x.coords[0][0]) + node_df['y'] = nodes_gdf.geometry.apply(lambda x: x.coords[0][1]) + node_records = node_df.drop_duplicates(subset=['x', 'y']).to_records(index=False) + + sql = "insert into nodes(node_id, is_centroid, modes, link_types, ovm_id, geometry) Values(?, ?, ?, ?, ?, MakePoint(?,?, 4326))" + self.conn.executemany(sql, node_records) + self.conn.commit() + del nodes_gdf + + all_attrs = links_gdf.values.tolist() + + + insert_qry = """INSERT INTO "links" ({}, geometry) VALUES({}, GeomFromWKB(?, 4326))""" + sql = insert_qry.format(field_names, ",".join(["?"] * (len(link_order) - 1))) + self.logger.info("Adding network links") + self.__emit_all(["text", "Adding network links"]) + try: + self.curr.executemany(sql, all_attrs) + except Exception as e: + self.logger.error("error when inserting link {}. Error {}".format(all_attrs[0], e.args)) + self.logger.error(sql) + raise e + + self.conn.commit() + del links_gdf + self.curr.close() + + def __worksetup(self): + self.__link_types = self.project.network.link_types + lts = self.__link_types.all_types() + for lt_id, lt in lts.items(): + self.__model_link_types.append(lt.link_type) + self.__model_link_type_ids.append(lt_id) + + def __repair_link_type(self, link_type: str) -> str: + original_link_type = link_type + link_type = "".join([x for x in link_type if x in string.ascii_letters + "_"]).lower() + split = link_type.split("_") + for i, piece in enumerate(split[1:]): + if piece in ["link", "segment", "stretch"]: + link_type = "_".join(split[0 : i + 1]) + + if len(link_type) == 0: + link_type = "empty" + + if len(self.__model_link_type_ids) >= 51 and link_type not in self.__model_link_types: + link_type = "aggregate_link_type" + + if link_type in self.__model_link_types: + lt = self.__link_types.get_by_name(link_type) + if original_link_type not in lt.description: + lt.description += f", {original_link_type}" + lt.save() + self.__link_type_quick_reference[original_link_type.lower()] = link_type + return link_type + + letter = link_type[0] + if letter in self.__model_link_type_ids: + letter = letter.upper() + if letter in self.__model_link_type_ids: + for letter in string.ascii_letters: + if letter not in self.__model_link_type_ids: + break + letter + lt = self.__link_types.new(letter) + lt.link_type = link_type + lt.description = f"Link types from Overture Maps: {original_link_type}" + lt.save() + self.__model_link_types.append(link_type) + self.__model_link_type_ids.append(letter) + self.__link_type_quick_reference[original_link_type.lower()] = link_type + return link_type + + def create_node_ids(self, data_frame: gpd.GeoDataFrame) -> pd.Series: + """ + Creates node_ids as well as the self.nodes and self.node_ids dictories + """ + node_ids = [] + data_frame["node_id"] = 1 + for i in range(len(data_frame)): + node_count = i + self.node_start + node_ids.append(node_count) + self.node_ids[node_count] = { + "ovm_id": data_frame["ovm_id"][i], + "lat": data_frame["geometry"][i].y, + "lon": data_frame["geometry"][i].x, + "coord": (data_frame["geometry"][i].x, data_frame["geometry"][i].y), + } + self.nodes[data_frame["ovm_id"][i]] = { + "lat": data_frame["geometry"][i].y, + "lon": data_frame["geometry"][i].x, + "coord": (data_frame["geometry"][i].x, data_frame["geometry"][i].y), + "node_id": node_count, + } + data_frame["node_id"] = pd.Series(node_ids) + return data_frame["node_id"] + + def modes_per_link_type(self): + p = Parameters(self.project) + modes = p.parameters["network"]["ovm"]["modes"] + result = [(key, key[0]) for key in modes.keys()] + mode_codes = {p[0]: p[1] for p in result} + type_list = {} + notfound = "" + for mode, val in modes.items(): + all_types = val["link_types"] + md = mode_codes[mode] + for tp in all_types: + type_list[tp] = "{}{}".format(type_list.get(tp, ""), md) + if val["unknown_tags"]: + notfound += md + + type_list = {k: "".join(set(v)) for k, v in type_list.items()} + return type_list, "{}".format(notfound) + + def trim_geometry(self, node_lu: dict, row: dict) -> shapely.LineString: + lat_long_a = node_lu[row["a_node"]]["coord"] + lat_long_b = node_lu[row["b_node"]]["coord"] + start, end = -1, -1 + for j, coord in enumerate(row.geometry.coords): + if lat_long_a == coord: + start = j + if lat_long_b == coord: + end = j + if start < 0 or end < 0: + raise RuntimeError("Couldn't find the start end coords in the given linestring") + return shapely.LineString(row.geometry.coords[start : end + 1]) + + # Function to process each row and create a new GeoDataFrame + def split_connectors(self, row: dict) -> gpd.GeoDataFrame: + # Extract necessary information from the row + connectors = row["connectors"] + + direction_dictionary = self.get_direction(row["direction"]) + # Check if 'Connectors' has more than 2 elements + if np.size(connectors) >= 2: + # Split the DataFrame into multiple rows + rows = [] + + for i in range(len(connectors) - 1): + new_row = { + "a_node": self.nodes[connectors[i]]["node_id"], + "b_node": self.nodes[connectors[i + 1]]["node_id"], + "direction": direction_dictionary["direction"], + "link_type": self.__link_type_quick_reference.get( + row["link_type"].lower(), self.__repair_link_type(row["link_type"]) + ), + "name": row["name"], + "speed_ab": self.get_speed(row["speed"]), + "ovm_id": row["ovm_id"], + "geometry": row["geometry"], + "lanes_ab": direction_dictionary["lanes_ab"], + "lanes_ba": direction_dictionary["lanes_ba"], + } + rows.append(new_row) + processed_df = gpd.GeoDataFrame(rows) + else: + raise ValueError("Invalid amount of connectors provided. Must be 2< to be considered a link.") + return processed_df + + def get_speed(self, speed_row) -> float: + """ + This function returns the speed of a road, if they have multiple speeds listed it will total the speeds listed by the proportions of the road they makeup. + """ + if speed_row == None: + adjusted_speed = speed_row + else: + speed = json.loads(speed_row) + if type(speed) == dict: + adjusted_speed = speed["maxSpeed"][0] + elif type(speed) == list and len(speed) >= 1: + # Extract the 'at' list from each dictionary + # eg [[0.0, 0.064320774], [0.064320774, 1.0]] + at_values_list = [entry["at"] for entry in speed] + + # Calculate differences between consecutive numbers in each 'at' list. This list iterates through each 'at' + # list in at_values_list and calculates the difference between consecutive elements using (at[i + 1] - at[i]). + # The result is a flat list of differences for all 'at' lists. + # eg [0.064320774, 0.935679226] + differences = [ + diff for at in at_values_list for diff in (at[i + 1] - at[i] for i in range(len(at) - 1)) + ] + + new_list = [] + for element in differences: + # Find the index of the value in the differences list + index_d = differences.index(element) + + # Access the corresponding entry in the original 'data' list to access the 'maxSpeed' value + speed_segment = speed[index_d]["maxSpeed"][0] * element + new_list.append(speed_segment) + + adjusted_speed = round(sum(new_list), 2) + return adjusted_speed + + def __update_table_structure(self): + curr = self.conn.cursor() + curr.execute("pragma table_info(Links)") + structure = curr.fetchall() + has_fields = [x[1].lower() for x in structure] + fields = [field.lower() for field in self.get_link_fields()] + ["ovm_id"] + for field in [f for f in fields if f not in has_fields]: + ltype = self.get_link_field_type(field).upper() + curr.execute(f"Alter table Links add column {field} {ltype}") + self.conn.commit() + + @staticmethod + def get_link_fields(): + p = Parameters() + fields = p.parameters["network"]["links"]["fields"] + owf = [list(x.keys())[0] for x in fields["one-way"]] + + twf1 = ["{}_ab".format(list(x.keys())[0]) for x in fields["two-way"]] + twf2 = ["{}_ba".format(list(x.keys())[0]) for x in fields["two-way"]] + + return owf + twf1 + twf2 + ["ovm_id"] + + @staticmethod + def get_link_field_type(field_name: list): + p = Parameters() + fields = p.parameters["network"]["links"]["fields"] + + if field_name[-3:].lower() in ["_ab", "_ba"]: + field_name = field_name[:-3] + for tp in fields["two-way"]: + if field_name in tp: + return tp[field_name]["type"] + else: + for tp in fields["one-way"]: + if field_name in tp: + return tp[field_name]["type"] + + @staticmethod + def get_direction(directions_list: list): + new_list = [] + at_dictionary = {} + + # Dictionary mapping direction strings to numeric values or descriptions + direction_dict = { + "forward": 1, + "backward": -1, + "bothWays": 0, + "alternating": "Travel is one-way and changes between forward and backward constantly", + "reversible": "Travel is one-way and changes between forward and backward infrequently", + } + + # Lambda function to check numbers and create a new dictionary + check_numbers = lambda lst: { + "direction": 1 if all(x == 1 for x in lst) else -1 if all(x == -1 for x in lst) else 0, + "lanes_ab": lst.count(1) if 1 in lst else None, + "lanes_ba": lst.count(-1) if -1 in lst else None, + } + + if directions_list is None: + new_list = [-1, 1] + elif directions_list != None: + for direct in directions_list: + if type(direct) == dict: + # Extract direction from the dictionary and append to new_list + direction = direction_dict[direct["direction"]] + new_list.append(direction) + elif type(direct) == list: + a_list = [] + at_dictionary[str(direct[0]["at"])] = direct[0]["at"][1] - direct[0]["at"][0] + max_key = max(at_dictionary, key=at_dictionary.get) + a_list.append(max_key) + + # Check if the current list is the one with maximum 'at' range + if str(direct[0]["at"]) == a_list[-1]: + new_list.clear() + for lists in direct[0]["value"]: + direction = direction_dict[lists["direction"]] + new_list.append(direction) + + return check_numbers(lst=new_list) diff --git a/aequilibrae/project/network/ovm_downloader.py b/aequilibrae/project/network/ovm_downloader.py new file mode 100644 index 000000000..34ee489c1 --- /dev/null +++ b/aequilibrae/project/network/ovm_downloader.py @@ -0,0 +1,235 @@ +import json +import importlib.util as iutil +import sqlite3 +import logging +from pathlib import Path +import string + +from aequilibrae.context import get_active_project +from aequilibrae.parameters import Parameters +from aequilibrae.project.network.link_types import LinkTypes +from aequilibrae.context import get_logger +import importlib.util as iutil +from aequilibrae.utils.spatialite_utils import connect_spatialite +from aequilibrae.project.network.haversine import haversine +from aequilibrae.utils import WorkerThread + +# from .haversine import haversine +# from ...utils import WorkerThread + +import duckdb +import shapely +import geopandas as gpd +import pandas as pd +import numpy as np +from typing import Union +from shapely.geometry import LineString, Point + +DEFAULT_OVM_S3_LOCATION = "s3://overturemaps-us-west-2/release/2023-11-14-alpha.0//theme=transportation" + +spec = iutil.find_spec("PyQt5") +pyqt = spec is not None +if pyqt: + from PyQt5.QtCore import pyqtSignal + +spec = iutil.find_spec("qgis") +isqgis = spec is not None +if isqgis: + import qgis + +class OVMDownloader(WorkerThread): + if pyqt: + downloading = pyqtSignal(object) + + def __emit_all(self, *args): + if pyqt: + self.downloading.emit(*args) + + def __init__(self, modes: list, project_path: Union[str, Path], logger: logging.Logger = None) -> None: + WorkerThread.__init__(self, None) + self.logger = logger or get_logger() + self.filter = self.get_ovm_filter(modes) + self.GeoDataFrame = [] + self.__project_path = Path(project_path) + self.pth = str(self.__project_path).replace("\\", "/") + self.insert_qry = """INSERT INTO {} ({}, geometry) VALUES({}, GeomFromText(?, 4326))""" + + def initialise_duckdb_spatial(self): + conn = duckdb.connect() + c = conn.cursor() + + c.execute( + """INSTALL spatial; + INSTALL httpfs; + INSTALL parquet; + """ + ) + c.execute( + """LOAD spatial; + LOAD parquet; + SET s3_region='us-west-2'; + """ + ) + return c + + def downloadPlace(self, source, local_file_path=None): + pth = str(self.__project_path / "new_geopackage_pla.parquet").replace("\\", "/") + + if source == "s3": + data_source = "s3://overturemaps-us-west-2/release/2023-11-14-alpha.0/theme=places/type=*" + elif source == "local": + data_source = local_file_path.replace("\\", "/") + else: + raise ValueError("Invalid source. Use 's3' or provide a valid local file path.") + + sql = f""" + COPY( + SELECT + id, + CAST(names AS JSON) AS name, + CAST(categories AS JSON) AS categories, + CAST(brand AS JSON) AS brand, + CAST(addresses AS JSON) AS addresses, + ST_GeomFromWKB(geometry) AS geom + FROM read_parquet('{data_source}/*', filename=true, hive_partitioning=1) + WHERE bbox.minx > '{self.bbox[0]}' + AND bbox.maxx < '{self.bbox[2]}' + AND bbox.miny > '{self.bbox[1]}' + AND bbox.maxy < '{self.bbox[3]}') + TO '{pth}'; + """ + + c = self.initialise_duckdb_spatial() + c.execute(sql) + + + def downloadTransportation(self, bbox: list, data_source: Union[str, Path], output_dir: Union[str, Path]): + data_source = Path(data_source) or DEFAULT_OVM_S3_LOCATION + output_dir = Path(output_dir) + + output_file_link = output_dir / f'type=segment' / f'transportation_data_segment.parquet' + output_file_node = output_dir / f'type=connector' / f'transportation_data_connector.parquet' + # output_file = output_dir / f'type={t}' / f'transportation_data_{t}.parquet' + output_file_link.parent.mkdir(parents=True, exist_ok=True) + output_file_node.parent.mkdir(parents=True, exist_ok=True) + + # Uncomment to see what information is stored the parquet file + # sql = f""" + # DESCRIBE + # SELECT + # road + # FROM read_parquet('{data_source}/type=segment/*', union_by_name=True) + # """ + # c = self.initialise_duckdb_spatial() + # g = c.execute(sql) + # print(g.df()) + + sql_link = f""" + COPY ( + SELECT + id AS ovm_id, + connectors, + CAST(road AS JSON) ->>'lanes' AS direction, + CAST(road AS JSON) ->>'class' AS link_type, + CAST(road AS JSON) ->>'roadNames' ->>'common' AS name, + CAST(road AS JSON) ->>'restrictions' ->> 'speedLimits' AS speed, + road, + geometry + FROM read_parquet('{data_source}/type=segment/*', union_by_name=True) + WHERE bbox.minx > '{bbox[0]}' + AND bbox.maxx < '{bbox[2]}' + AND bbox.miny > '{bbox[1]}' + AND bbox.maxy < '{bbox[3]}') + TO '{output_file_link}' + (FORMAT 'parquet', COMPRESSION 'ZSTD'); + """ + c = self.initialise_duckdb_spatial() + c.execute(sql_link) + + sql_node = f""" + COPY ( + SELECT + id AS ovm_id, + geometry + FROM read_parquet('{data_source}/type=connector/*', union_by_name=True) + WHERE bbox.minx > '{bbox[0]}' + AND bbox.maxx < '{bbox[2]}' + AND bbox.miny > '{bbox[1]}' + AND bbox.maxy < '{bbox[3]}') + TO '{output_file_node}' + (FORMAT 'parquet', COMPRESSION 'ZSTD'); + """ + c.execute(sql_node) + + # Creating links GeoDataFrame + df_link = pd.read_parquet(output_file_link) + geo_link = gpd.GeoSeries.from_wkb(df_link.geometry, crs=4326) + gdf_link = gpd.GeoDataFrame(df_link,geometry=geo_link) + + # Creating nodes GeoDataFrame + df_node = pd.read_parquet(output_file_node) + geo_node = gpd.GeoSeries.from_wkb(df_node.geometry, crs=4326) + gdf_node = gpd.GeoDataFrame(df_node,geometry=geo_node) + + return gdf_link, gdf_node + + def download_test_data(self, data_source: Union[str, Path]): + '''This method only used to seed/bootstrap a local copy of a small test data set''' + airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ] + # brisbane_bbox = [153.1771, -27.6851, 153.2018, -27.6703] + data_source = data_source.replace("\\", "/") + + + for t in ['segment','connector']: + (Path(__file__).parent.parent.parent.parent / "tests" / "data" / "overture" / "theme=transportation" / f'type={t}').mkdir(parents=True, exist_ok=True) + pth1 = Path(__file__).parent.parent.parent.parent / "tests" / "data" / "overture" / "theme=transportation" / f"type={t}" / f'airlie_beach_transportation_{t}.parquet' + sql = f""" + COPY ( + SELECT + * + FROM read_parquet('{data_source}/type={t}/*', union_by_name=True) + WHERE bbox.minx > '{airlie_bbox[0]}' + AND bbox.maxx < '{airlie_bbox[2]}' + AND bbox.miny > '{airlie_bbox[1]}' + AND bbox.maxy < '{airlie_bbox[3]}') + TO '{pth1}' + (FORMAT 'parquet', COMPRESSION 'ZSTD'); + """ + c = self.initialise_duckdb_spatial() + c.execute(sql) + + df = pd.read_parquet(Path(pth1)) + geo = gpd.GeoSeries.from_wkb(df.geometry, crs=4326) + gdf = gpd.GeoDataFrame(df,geometry=geo) + gdf.to_parquet(Path(pth1)) + # return gdf + + def get_ovm_filter(self, modes: list) -> str: + """ + loosely adapted from http://www.github.com/gboeing/osmnx + """ + + p = Parameters().parameters["network"]["ovm"] + all_tags = p["all_link_types"] + + p = p["modes"] + all_modes = list(p.keys()) + + tags_to_keep = [] + for m in modes: + if m not in all_modes: + raise ValueError(f"Mode {m} not listed in the parameters file") + tags_to_keep += p[m]["link_types"] + tags_to_keep = list(set(tags_to_keep)) + + # Default to remove + service = '["service"!~"parking|parking_aisle|driveway|private|emergency_access"]' + access = '["access"!~"private"]' + + filtered = [x for x in all_tags if x not in tags_to_keep] + filtered = "|".join(filtered) + + filter = f'["area"!~"yes"]["highway"!~"{filtered}"]{service}{access}' + + return filter + \ No newline at end of file diff --git a/docs/source/examples/creating_models/from_osm.py b/docs/source/examples/creating_models/from_osm.py index 93d634349..fbbdf8758 100644 --- a/docs/source/examples/creating_models/from_osm.py +++ b/docs/source/examples/creating_models/from_osm.py @@ -55,6 +55,8 @@ curr = project.conn.cursor() curr.execute("select avg(xmin), avg(ymin) from idx_links_geometry") long, lat = curr.fetchone() +print(long) +print(lat) # %% map_osm = folium.Map(location=[lat, long], zoom_start=14) diff --git a/docs/source/examples/creating_models/from_ovm.py b/docs/source/examples/creating_models/from_ovm.py new file mode 100644 index 000000000..0675e1b77 --- /dev/null +++ b/docs/source/examples/creating_models/from_ovm.py @@ -0,0 +1,72 @@ +""" +Project from Overture Maps +============================= +In this example, we show how to create an empty project and populate it with a network from Overture Maps. +We will use Folium to visualize the network. +""" + +# %% +# Imports +from pathlib import Path +from uuid import uuid4 +from tempfile import gettempdir +from os.path import join +from aequilibrae import Project +import folium + +# %% +# We create an empty project on an arbitrary folder +from shutil import rmtree + +fldr = join(gettempdir(), uuid4().hex) +project = Project() +project.new(fldr) +# %% +# Now we can download the network from any place in the world (as long as you have memory for all the download +# and data wrangling that will be done) +# We have stored Airlie Beach's transportation parquet files in the folder with the file path data_source below as using the cloud-native Parquet files takes a much longer time to run +# We recommend downloading these cloud-native Parquet files to drive and replacing the data_source file to match +dir = str(Path('../../../../').resolve()) +data_source = Path(dir) / 'tests' / 'data' / 'overture' / 'theme=transportation' +output_dir = Path(fldr) / "raw_parquet" + +# For the sake of this example, we will choose the small town of Airlie Beach. +# The "bbox" parameter specifies the bounding box encompassing the desired geographical location. In the given example, this refers to the bounding box that encompasses Airlie Beach. +bbox = [148.7077, -20.2780, 148.7324, -20.2621 ] + +# We can create from a bounding box or a named place. +project.network.create_from_ovm(west=bbox[0], south=bbox[1], east=bbox[2], north=bbox[3], data_source=data_source, output_dir=output_dir) + +# %% +# We grab all the links data as a Pandas DataFrame so we can process it easier +links = project.network.links.data + +# %% +# We create a Folium layer +network_links = folium.FeatureGroup("links") + +# We do some Python magic to transform this dataset into the format required by Folium +# We are only getting link_id and link_type into the map, but we could get other pieces of info as well +for i, row in links.iterrows(): + points = row.geometry.wkt.replace("LINESTRING ", "").replace("(", "").replace(")", "").split(", ") + points = "[[" + "],[".join([p.replace(" ", ", ") for p in points]) + "]]" + # we need to take from x/y to lat/long + points = [[x[1], x[0]] for x in eval(points)] + + line = folium.vector_layers.PolyLine( + points, popup=f"link_id: {row.link_id}", tooltip=f"{row.link_type}", color="blue", weight=10 + ).add_to(network_links) + +# %% +# We get the center of the region +long = (bbox[0]+bbox[2])/2 +lat = (bbox[1]+bbox[3])/2 + +# %% +map_osm = folium.Map(location=[lat, long], zoom_start=14) +network_links.add_to(map_osm) +folium.LayerControl().add_to(map_osm) +map_osm + +# %% +project.close() diff --git a/docs/source/examples/creating_models/ovm.ipynb b/docs/source/examples/creating_models/ovm.ipynb new file mode 100644 index 000000000..666af0ff9 --- /dev/null +++ b/docs/source/examples/creating_models/ovm.ipynb @@ -0,0 +1,10010 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "from uuid import uuid4\n", + "from tempfile import gettempdir\n", + "from os.path import join\n", + "from aequilibrae import Project\n", + "import folium\n", + "\n", + "from aequilibrae.project.network.ovm_downloader import OVMDownloader\n", + "# sphinx_gallery_thumbnail_path = 'images/nauru.png'" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# We create an empty project on an arbitrary folder\n", + "fldr = join(gettempdir(), uuid4().hex)\n", + "project = Project()\n", + "project.new(fldr)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Now we can download the network from any place in the world (as long as you have memory for all the download\n", + "# and data wrangling that will be done)\n", + "\n", + "# We can create from a bounding box or a named place.\n", + "# For the sake of this example, we will choose a section of highway in Brisebane.\n", + "brisbane_bbox = [153.1771, -27.6851, 153.2018, -27.6703]\n", + "project.network.create_from_ovm(west=brisbane_bbox[0], south=brisbane_bbox[1], east=brisbane_bbox[2], north=brisbane_bbox[3], data_source=r'C:\\Users\\penny\\git\\data\\theme=transportation', output_dir=r'C:\\Users\\penny\\git\\Aequilibrae\\tests\\data\\overture\\theme=transportation')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# We grab all the links data as a Pandas DataFrame so we can process it easier\n", + "links = project.network.links.data\n", + "\n", + "# We create a Folium layer\n", + "network_links = folium.FeatureGroup(\"links\")\n", + "\n", + "# We do some Python magic to transform this dataset into the format required by Folium\n", + "# We are only getting link_id and link_type into the map, but we could get other pieces of info as well\n", + "for i, row in links.iterrows():\n", + " points = row.geometry.wkt.replace(\"LINESTRING \", \"\").replace(\"(\", \"\").replace(\")\", \"\").split(\", \")\n", + " points = \"[[\" + \"],[\".join([p.replace(\" \", \", \") for p in points]) + \"]]\"\n", + " # we need to take from x/y to lat/long\n", + " points = [[x[1], x[0]] for x in eval(points)]\n", + "\n", + " line = folium.vector_layers.PolyLine(\n", + " points, popup=f\"link_id: {row.link_id}\", tooltip=f\"{row.link_type}\", color=\"blue\", weight=10\n", + " ).add_to(network_links)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# We get the center of the region we are working with some SQL magic\n", + "# long = (bbox[0]+bbox[2])/2\n", + "# lat = (bbox[1]+bbox[3])/2\n", + "long = (brisbane_bbox[0]+brisbane_bbox[2])/2\n", + "lat = (brisbane_bbox[1]+brisbane_bbox[3])/2" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "map_osm = folium.Map(location=[lat, long], zoom_start=14)\n", + "network_links.add_to(map_osm)\n", + "folium.LayerControl().add_to(map_osm)\n", + "map_osm" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "project.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements.txt b/requirements.txt index 6a1ba0519..ade9e5a0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,6 @@ shapely pandas pyproj rtree -openmatrix \ No newline at end of file +openmatrix +duckdb +geopandas \ No newline at end of file diff --git a/tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb b/tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb new file mode 100644 index 000000000..f75abba08 --- /dev/null +++ b/tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb @@ -0,0 +1,433 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import uuid\n", + "import zipfile\n", + "from os.path import join, dirname\n", + "from tempfile import gettempdir\n", + "from unittest import TestCase\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "from aequilibrae import TrafficAssignment, TrafficClass, Graph, Project\n", + "from tests.data import siouxfalls_project\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", + " build_compressed_graph(self)\n", + "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", + " build_compressed_graph(self)\n", + "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", + " build_compressed_graph(self)\n", + "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", + " build_compressed_graph(self)\n" + ] + } + ], + "source": [ + "proj_path ='coming'\n", + "# Initialise project:\n", + "project = Project()\n", + "project.open(proj_path)\n", + "project.network.build_graphs()\n", + "car_graph = project.network.graphs[\"c\"] # type: Graph\n", + "\n", + "car_graph.set_graph(\"free_flow_time\")\n", + "car_graph.set_blocked_centroid_flows(False)\n", + "matrix = project.matrices.get_matrix(\"demand_omx\")\n", + "matrix.computational_view()\n", + "\n", + "# Extra data specific to ODME:\n", + "index = car_graph.nodes_to_indices\n", + "dims = matrix.matrix_view.shape\n", + "count_vol_cols = [\"class\", \"link_id\", \"direction\", \"obs_volume\"]\n", + "\n", + "# Initial assignment parameters:\n", + "assignment = TrafficAssignment()\n", + "assignclass = TrafficClass(\"car\", car_graph, matrix)\n", + "assignment.set_classes([assignclass])\n", + "assignment.set_vdf(\"BPR\")\n", + "assignment.set_vdf_parameters({\"alpha\": 0.15, \"beta\": 4.0})\n", + "assignment.set_vdf_parameters({\"alpha\": \"b\", \"beta\": \"power\"})\n", + "assignment.set_capacity_field(\"capacity\")\n", + "assignment.set_time_field(\"free_flow_time\")\n", + "assignment.max_iter = 5\n", + "assignment.set_algorithm(\"msa\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Get original flows:.\n", + "assignment.execute()\n", + "assign_df = assignment.results().reset_index(drop=False).fillna(0)\n", + "# SQUISH EXTRA DIMENSION FOR NOW - DEAL WITH THIS PROPERLY LATER ON!!!\n", + "matrix.matrix_view = np.squeeze(matrix.matrix_view, axis=2)\n", + "\n", + "# Set the observed count volumes:\n", + "flow = lambda i: assign_df.loc[assign_df[\"link_id\"] == i, \"matrix_ab\"].values[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
link_idmatrix_abmatrix_bamatrix_totCongested_Time_ABCongested_Time_BACongested_Time_MaxDelay_factor_ABDelay_factor_BADelay_factor_MaxVOC_ABVOC_BAVOC_maxPCE_ABPCE_BAPCE_tot
015880.00.05880.06.0023910.06.0023911.0003980.01.0003980.2270250.00.2270255880.00.05880.0
1210540.00.010540.04.0246830.04.0246831.0061710.01.0061710.4503610.00.45036110540.00.010540.0
236540.00.06540.06.0036590.06.0036591.0006100.01.0006100.2525080.00.2525086540.00.06540.0
346000.00.06000.06.6083360.06.6083361.3216670.01.3216671.2101210.01.2101216000.00.06000.0
459880.00.09880.04.0190570.04.0190571.0047640.01.0047640.4221600.00.4221609880.00.09880.0
...................................................
717211460.00.011460.020.5580700.020.5580705.1395170.05.1395172.2920000.02.29200011460.00.011460.0
727311320.00.011320.09.4056340.09.4056344.7028170.04.7028172.2290010.02.22900111320.00.011320.0
737411680.00.011680.020.6196870.020.6196875.1549220.05.1549222.2941290.02.29412911680.00.011680.0
74758860.00.08860.07.8681280.07.8681282.6227090.02.6227091.8135830.01.8135838860.00.08860.0
757612260.00.012260.012.1891610.012.1891616.0945800.06.0945802.4140950.02.41409512260.00.012260.0
\n", + "

76 rows × 16 columns

\n", + "
" + ], + "text/plain": [ + " link_id matrix_ab matrix_ba matrix_tot Congested_Time_AB \\\n", + "0 1 5880.0 0.0 5880.0 6.002391 \n", + "1 2 10540.0 0.0 10540.0 4.024683 \n", + "2 3 6540.0 0.0 6540.0 6.003659 \n", + "3 4 6000.0 0.0 6000.0 6.608336 \n", + "4 5 9880.0 0.0 9880.0 4.019057 \n", + ".. ... ... ... ... ... \n", + "71 72 11460.0 0.0 11460.0 20.558070 \n", + "72 73 11320.0 0.0 11320.0 9.405634 \n", + "73 74 11680.0 0.0 11680.0 20.619687 \n", + "74 75 8860.0 0.0 8860.0 7.868128 \n", + "75 76 12260.0 0.0 12260.0 12.189161 \n", + "\n", + " Congested_Time_BA Congested_Time_Max Delay_factor_AB Delay_factor_BA \\\n", + "0 0.0 6.002391 1.000398 0.0 \n", + "1 0.0 4.024683 1.006171 0.0 \n", + "2 0.0 6.003659 1.000610 0.0 \n", + "3 0.0 6.608336 1.321667 0.0 \n", + "4 0.0 4.019057 1.004764 0.0 \n", + ".. ... ... ... ... \n", + "71 0.0 20.558070 5.139517 0.0 \n", + "72 0.0 9.405634 4.702817 0.0 \n", + "73 0.0 20.619687 5.154922 0.0 \n", + "74 0.0 7.868128 2.622709 0.0 \n", + "75 0.0 12.189161 6.094580 0.0 \n", + "\n", + " Delay_factor_Max VOC_AB VOC_BA VOC_max PCE_AB PCE_BA PCE_tot \n", + "0 1.000398 0.227025 0.0 0.227025 5880.0 0.0 5880.0 \n", + "1 1.006171 0.450361 0.0 0.450361 10540.0 0.0 10540.0 \n", + "2 1.000610 0.252508 0.0 0.252508 6540.0 0.0 6540.0 \n", + "3 1.321667 1.210121 0.0 1.210121 6000.0 0.0 6000.0 \n", + "4 1.004764 0.422160 0.0 0.422160 9880.0 0.0 9880.0 \n", + ".. ... ... ... ... ... ... ... \n", + "71 5.139517 2.292000 0.0 2.292000 11460.0 0.0 11460.0 \n", + "72 4.702817 2.229001 0.0 2.229001 11320.0 0.0 11320.0 \n", + "73 5.154922 2.294129 0.0 2.294129 11680.0 0.0 11680.0 \n", + "74 2.622709 1.813583 0.0 1.813583 8860.0 0.0 8860.0 \n", + "75 6.094580 2.414095 0.0 2.414095 12260.0 0.0 12260.0 \n", + "\n", + "[76 rows x 16 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "assign_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "acc84914dd5d49aef4abd59151ad776eeaa26fd8e748105fd8228ce1c06cbf3b" + }, + "kernelspec": { + "display_name": "Python 3.10.12 ('venv': venv)", + "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.11.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/aequilibrae/project/ovm/setup_test_data.ipynb b/tests/aequilibrae/project/ovm/setup_test_data.ipynb new file mode 100644 index 000000000..840cd8f81 --- /dev/null +++ b/tests/aequilibrae/project/ovm/setup_test_data.ipynb @@ -0,0 +1,157 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test Setup\n", + "\n", + "This notebook is used to download and explore the OVM data sets for use in our automated testing environment." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys\n", + "from pathlib import Path\n", + "import pandas as pd\n", + "import geopandas as gpd\n", + "import shapely\n", + "from aequilibrae.project.network.ovm_downloader import OVMDownloader\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C:\\Users\\penny\\git\\Aequilibrae\n", + "['c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\tests\\\\aequilibrae\\\\project\\\\ovm', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311\\\\python311.zip', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311\\\\DLLs', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311\\\\Lib', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv', '', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages', 'C:\\\\Users\\\\penny\\\\git\\\\Aequilibrae', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages\\\\win32', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages\\\\win32\\\\lib', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages\\\\Pythonwin']\n", + "C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\n" + ] + }, + { + "ename": "AttributeError", + "evalue": "'OVMDownloader' object has no attribute 'replace'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[3], line 19\u001b[0m\n\u001b[0;32m 14\u001b[0m \u001b[38;5;28mprint\u001b[39m(test_data_dir)\n\u001b[0;32m 17\u001b[0m ovm_downloader_instance \u001b[38;5;241m=\u001b[39m OVMDownloader([\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcar\u001b[39m\u001b[38;5;124m\"\u001b[39m],test_data_dir)\n\u001b[1;32m---> 19\u001b[0m \u001b[43movm_downloader_instance\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdownload_test_data\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mE:/theme=transportation/type=segment\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\git\\Aequilibrae\\aequilibrae\\project\\network\\ovm_downloader.py:116\u001b[0m, in \u001b[0;36mdownload_test_data\u001b[1;34m(data_source, test_data_location)\u001b[0m\n\u001b[0;32m 114\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 115\u001b[0m c\u001b[38;5;241m.\u001b[39mexecute(sql)\n\u001b[1;32m--> 116\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 117\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mAn error occurred: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[1;31mAttributeError\u001b[0m: 'OVMDownloader' object has no attribute 'replace'" + ] + } + ], + "source": [ + "\n", + "aeq_dir = str(Path('../../../../').resolve())\n", + "print(aeq_dir)\n", + "print(sys.path)\n", + "if aeq_dir not in sys.path:\n", + " sys.path.append(aeq_dir)\n", + "\n", + "\n", + "test_data_dir = Path(aeq_dir) / 'theme=transportation' / 'type=segment' \n", + "print(test_data_dir)\n", + "\n", + "\n", + "ovm_downloader_instance = OVMDownloader([\"car\"],test_data_dir)\n", + "\n", + "ovm_downloader_instance.download_test_data('E:/theme=transportation/type=segment')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/ovm.parquet'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[7], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m os\u001b[38;5;241m.\u001b[39mgetcwd()\n\u001b[1;32m----> 2\u001b[0m df \u001b[38;5;241m=\u001b[39m \u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m../data/ovm/type=transportation/type=segment/ovm.parquet\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 3\u001b[0m df\n\u001b[0;32m 4\u001b[0m \u001b[38;5;66;03m# gdf = gpd.GeoDOataFrame(data=df, geometry=df.geometry.apply(shapely.wkb.loads))\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:670\u001b[0m, in \u001b[0;36mread_parquet\u001b[1;34m(path, engine, columns, storage_options, use_nullable_dtypes, dtype_backend, filesystem, filters, **kwargs)\u001b[0m\n\u001b[0;32m 667\u001b[0m use_nullable_dtypes \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[0;32m 668\u001b[0m check_dtype_backend(dtype_backend)\n\u001b[1;32m--> 670\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mimpl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 671\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 672\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 673\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 674\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 675\u001b[0m \u001b[43m \u001b[49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 676\u001b[0m \u001b[43m \u001b[49m\u001b[43mdtype_backend\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdtype_backend\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 677\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 678\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 679\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:265\u001b[0m, in \u001b[0;36mPyArrowImpl.read\u001b[1;34m(self, path, columns, filters, use_nullable_dtypes, dtype_backend, storage_options, filesystem, **kwargs)\u001b[0m\n\u001b[0;32m 262\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m manager \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124marray\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 263\u001b[0m to_pandas_kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msplit_blocks\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m \u001b[38;5;66;03m# type: ignore[assignment]\u001b[39;00m\n\u001b[1;32m--> 265\u001b[0m path_or_handle, handles, filesystem \u001b[38;5;241m=\u001b[39m \u001b[43m_get_path_or_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 266\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 267\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 268\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 269\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 270\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 271\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 272\u001b[0m pa_table \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapi\u001b[38;5;241m.\u001b[39mparquet\u001b[38;5;241m.\u001b[39mread_table(\n\u001b[0;32m 273\u001b[0m path_or_handle,\n\u001b[0;32m 274\u001b[0m columns\u001b[38;5;241m=\u001b[39mcolumns,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 277\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 278\u001b[0m )\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:139\u001b[0m, in \u001b[0;36m_get_path_or_handle\u001b[1;34m(path, fs, storage_options, mode, is_dir)\u001b[0m\n\u001b[0;32m 129\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[0;32m 131\u001b[0m \u001b[38;5;129;01mnot\u001b[39;00m fs\n\u001b[0;32m 132\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_dir\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 137\u001b[0m \u001b[38;5;66;03m# fsspec resources can also point to directories\u001b[39;00m\n\u001b[0;32m 138\u001b[0m \u001b[38;5;66;03m# this branch is used for example when reading from non-fsspec URLs\u001b[39;00m\n\u001b[1;32m--> 139\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[43mget_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 140\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_handle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mis_text\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\n\u001b[0;32m 141\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 142\u001b[0m fs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 143\u001b[0m path_or_handle \u001b[38;5;241m=\u001b[39m handles\u001b[38;5;241m.\u001b[39mhandle\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\common.py:872\u001b[0m, in \u001b[0;36mget_handle\u001b[1;34m(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)\u001b[0m\n\u001b[0;32m 863\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mopen\u001b[39m(\n\u001b[0;32m 864\u001b[0m handle,\n\u001b[0;32m 865\u001b[0m ioargs\u001b[38;5;241m.\u001b[39mmode,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 868\u001b[0m newline\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 869\u001b[0m )\n\u001b[0;32m 870\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 871\u001b[0m \u001b[38;5;66;03m# Binary mode\u001b[39;00m\n\u001b[1;32m--> 872\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mhandle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mioargs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 873\u001b[0m handles\u001b[38;5;241m.\u001b[39mappend(handle)\n\u001b[0;32m 875\u001b[0m \u001b[38;5;66;03m# Convert BytesIO or file objects passed with an encoding\u001b[39;00m\n", + "\u001b[1;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/ovm.parquet'" + ] + } + ], + "source": [ + "\n", + "os.getcwd()\n", + "df = pd.read_parquet('../data/ovm/type=transportation/type=segment/ovm.parquet')\n", + "df\n", + "# gdf = gpd.GeoDOataFrame(data=df, geometry=df.geometry.apply(shapely.wkb.loads))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "[Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[8], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m df \u001b[38;5;241m=\u001b[39m \u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 2\u001b[0m gdf \u001b[38;5;241m=\u001b[39m gpd\u001b[38;5;241m.\u001b[39mGeoDataFrame(data \u001b[38;5;241m=\u001b[39m df, geometry\u001b[38;5;241m=\u001b[39mdf\u001b[38;5;241m.\u001b[39mgeometry\u001b[38;5;241m.\u001b[39mapply(shapely\u001b[38;5;241m.\u001b[39mwkb\u001b[38;5;241m.\u001b[39mloads))\n\u001b[0;32m 4\u001b[0m gdf\u001b[38;5;241m.\u001b[39mplot()\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:670\u001b[0m, in \u001b[0;36mread_parquet\u001b[1;34m(path, engine, columns, storage_options, use_nullable_dtypes, dtype_backend, filesystem, filters, **kwargs)\u001b[0m\n\u001b[0;32m 667\u001b[0m use_nullable_dtypes \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[0;32m 668\u001b[0m check_dtype_backend(dtype_backend)\n\u001b[1;32m--> 670\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mimpl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 671\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 672\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 673\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 674\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 675\u001b[0m \u001b[43m \u001b[49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 676\u001b[0m \u001b[43m \u001b[49m\u001b[43mdtype_backend\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdtype_backend\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 677\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 678\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 679\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:265\u001b[0m, in \u001b[0;36mPyArrowImpl.read\u001b[1;34m(self, path, columns, filters, use_nullable_dtypes, dtype_backend, storage_options, filesystem, **kwargs)\u001b[0m\n\u001b[0;32m 262\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m manager \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124marray\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 263\u001b[0m to_pandas_kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msplit_blocks\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m \u001b[38;5;66;03m# type: ignore[assignment]\u001b[39;00m\n\u001b[1;32m--> 265\u001b[0m path_or_handle, handles, filesystem \u001b[38;5;241m=\u001b[39m \u001b[43m_get_path_or_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 266\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 267\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 268\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 269\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 270\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 271\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 272\u001b[0m pa_table \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapi\u001b[38;5;241m.\u001b[39mparquet\u001b[38;5;241m.\u001b[39mread_table(\n\u001b[0;32m 273\u001b[0m path_or_handle,\n\u001b[0;32m 274\u001b[0m columns\u001b[38;5;241m=\u001b[39mcolumns,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 277\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 278\u001b[0m )\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:139\u001b[0m, in \u001b[0;36m_get_path_or_handle\u001b[1;34m(path, fs, storage_options, mode, is_dir)\u001b[0m\n\u001b[0;32m 129\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[0;32m 131\u001b[0m \u001b[38;5;129;01mnot\u001b[39;00m fs\n\u001b[0;32m 132\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_dir\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 137\u001b[0m \u001b[38;5;66;03m# fsspec resources can also point to directories\u001b[39;00m\n\u001b[0;32m 138\u001b[0m \u001b[38;5;66;03m# this branch is used for example when reading from non-fsspec URLs\u001b[39;00m\n\u001b[1;32m--> 139\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[43mget_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 140\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_handle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mis_text\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\n\u001b[0;32m 141\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 142\u001b[0m fs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 143\u001b[0m path_or_handle \u001b[38;5;241m=\u001b[39m handles\u001b[38;5;241m.\u001b[39mhandle\n", + "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\common.py:872\u001b[0m, in \u001b[0;36mget_handle\u001b[1;34m(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)\u001b[0m\n\u001b[0;32m 863\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mopen\u001b[39m(\n\u001b[0;32m 864\u001b[0m handle,\n\u001b[0;32m 865\u001b[0m ioargs\u001b[38;5;241m.\u001b[39mmode,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 868\u001b[0m newline\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 869\u001b[0m )\n\u001b[0;32m 870\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 871\u001b[0m \u001b[38;5;66;03m# Binary mode\u001b[39;00m\n\u001b[1;32m--> 872\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mhandle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mioargs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 873\u001b[0m handles\u001b[38;5;241m.\u001b[39mappend(handle)\n\u001b[0;32m 875\u001b[0m \u001b[38;5;66;03m# Convert BytesIO or file objects passed with an encoding\u001b[39;00m\n", + "\u001b[1;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'" + ] + } + ], + "source": [ + "df = pd.read_parquet('../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet')\n", + "gdf = gpd.GeoDataFrame(data = df, geometry=df.geometry.apply(shapely.wkb.loads))\n", + "\n", + "gdf.plot()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "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.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/aequilibrae/project/ovm/test_ovm_downloader.py b/tests/aequilibrae/project/ovm/test_ovm_downloader.py new file mode 100644 index 000000000..5a870cb29 --- /dev/null +++ b/tests/aequilibrae/project/ovm/test_ovm_downloader.py @@ -0,0 +1,61 @@ +import importlib.util as iutil +import tempfile +from pathlib import Path +from tempfile import gettempdir, mkdtemp +from unittest import TestCase +import os +import geopandas as gpd +from aequilibrae.project.network.ovm_downloader import OVMDownloader +from random import random + +spec = iutil.find_spec("PyQt5") +pyqt = spec is not None + +test_data_dir = Path(__file__) + +data_dir = Path(__file__).parent.parent.parent.parent / "data" / "overture" / "theme=transportation" + + +class TestOVMDownloader(TestCase): + def setUp(self) -> None: + os.environ["PATH"] = os.path.join(gettempdir(), "temp_data") + ";" + os.environ["PATH"] + self.pth = Path(mkdtemp(prefix="aequilibrae")) + + def test_download(self): + # if not self.should_do_work(): + # return + o = OVMDownloader(["car"], self.pth) + with tempfile.TemporaryDirectory() as output_dir: + list_element = 0 + for t in ['segment', 'connector']: + output_dir = Path(output_dir) + bbo =[148.71641, -20.27082, 148.71861, -20.27001] + box1 = [148.713909, -20.272261, 148.7206475,-20.2702697] + woolworths_parkinglot = [148.718, -20.27049, 148.71889, -20.27006] + o.downloadTransportation(bbox=box1, data_source=data_dir, output_dir=output_dir) + list_gdf = o.g_dataframes + expected_file = output_dir / f'theme=transportation' / f"type={t}" / f"transportation_data_{t}.parquet" + assert expected_file.exists() + + gdf = list_gdf[list_element] + gdf_link = list_gdf[0] + gdf_node = list_gdf[1] + + assert gdf.shape[0] > 0 + + link_columns = ['ovm_id', 'connectors', 'direction', 'link_type', 'name', 'speed', 'road', 'geometry'] + for element in link_columns: + assert element in gdf_link.columns + + node_columns = ['ovm_id', 'geometry'] + for element in node_columns: + assert element in gdf_node.columns + + # assert 'is_centroid' in gdf_node.columns + # assert ['unknown', 'secondary', 'residential', 'parkingAisle'] == list(list_gdf[0]['link_type'].unique()) + + list_element+=1 + + def should_do_work(self): + thresh = 1.01 if os.environ.get("GITHUB_WORKFLOW", "ERROR") == "Code coverage" else 0.02 + return random() < thresh diff --git a/tests/aequilibrae/project/ovm/test_ovm_processor.py b/tests/aequilibrae/project/ovm/test_ovm_processor.py new file mode 100644 index 000000000..d4b5c34c2 --- /dev/null +++ b/tests/aequilibrae/project/ovm/test_ovm_processor.py @@ -0,0 +1,226 @@ +import copy +import json +import tempfile +import pandas as pd +from pathlib import Path +from tempfile import gettempdir, mkdtemp +import geopandas as gpd +import shapely +from aequilibrae.project.network.ovm_downloader import OVMDownloader +from unittest import TestCase +import os + +from uuid import uuid4 +from os.path import join +from aequilibrae import Project +from aequilibrae.project.network.ovm_builder import OVMBuilder + +class TestOVMProcessor(TestCase): + def setUp(self) -> None: + os.environ["PATH"] = os.path.join(gettempdir(), "temp_data") + ";" + os.environ["PATH"] + self.pth = Path(mkdtemp(prefix="aequilibrae")) + + self.fldr = join(gettempdir(), uuid4().hex) + self.project = Project() + self.project.new(self.fldr) + + def test_link_geo_trimmer(self): + node1 = (148.7165148, -20.273062) + node2 = (148.7164104, -20.2730078) + geo = shapely.LineString([(148.7165748, -20.2730668), node1, (148.7164585, -20.2730418), node2]) + link_gdf = gpd.GeoDataFrame([[1, 2, geo]], columns=["a_node", "b_node", "geometry"]) + new_geom = copy.copy(link_gdf) + + node_lu = {1: {'lat': node1[1], 'long': node1[0], 'coord': node1}, + 2: {'lat': node2[1], 'long': node2[0], 'coord': node2}} + + dataframes = [link_gdf, gpd.GeoDataFrame()] + o = OVMBuilder(ovm_download=dataframes, project_path=self.pth, project=self.project) + + # Iterate over the correct range + new_geom['geometry'] = [o.trim_geometry(node_lu, row) for e, row in link_gdf.iterrows()] + + + # Assuming you want to assert the length of the new geometry + assert len(new_geom['geometry'][0].coords) == 3 + + # Assuming you want to assert the correctness of the new geometry + # If you don't need the difference operation, you can skip it + + for i in range(0, len(link_gdf)): + if i > 0: + assert new_geom["geometry"][i] == shapely.LineString([node1, (148.7164585, -20.2730418), node2]) + + def test_link_lanes(self): + """ + segment and node infomation is currently [1] element of links when running from_ovm.py + """ + + no_info = None + simple = [{"direction": "backward"}, + {"direction": "forward"}] + + lanes_3 = [{'direction': 'forward', 'restrictions': {'access': [{'allowed': {'when': {'mode': ['hov']}}}], + 'minOccupancy': {'isAtLeast': 3}}}, + {'direction': 'forward'}, + {'direction': 'forward'}] + + highway = [{"direction": "backward"}, {"direction": "backward"}, + {"direction": "backward"}, {"direction": "backward"}, + {"direction": "forward"}, {"direction": "forward"}, + {"direction": "forward"}, {"direction": "forward"}] + + lane_ends = [[{'at': [0, 0.67], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}, + {'direction': 'forward'}]}], + [{'at': [0.67, 1], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}]}]] + + lane_begins = [[{'at': [0, 0.2], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}]}], + [{'at': [0.2, 1], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}, + {'direction': 'forward'}]}]] + + lane_merge_twice = [[{'at': [0, 0.2], + 'value':[ + {'direction': 'backward'}, + {'direction': 'backward'}, + {'direction': 'forward'}, + {'direction': 'forward'}]}], + [{'at': [0.2, 0.8], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}, + {'direction': 'forward'}]}], + [{'at': [0.8, 1], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}]}]] + + equal_dis = [[{'at': [0, 0.5], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}, + {'direction': 'forward'}]}], + [{'at': [0.5, 1], + 'value':[ + {'direction': 'backward'}, + {'direction': 'forward'}]}]] + + def road(lane): + road_info = str({"class":"secondary", + "surface":"paved", + "restrictions":{"speedLimits":{"maxSpeed":[70,"km/h"]}}, + "roadNames":{"common":[{"language":"local","value":"Shute Harbour Road"}]}, + "lanes": lane}) + return road_info + + a_node = {'ovm_id': '8f9d0e128cd9709-167FF64A37F1BFFB', 'geometry': shapely.Point(148.72460, -20.27472)} + b_node = {'ovm_id': '8f9d0e128cd98d6-15FFF68E65613FDF', 'geometry': shapely.Point(148.72471, -20.27492)} + node_df = gpd.GeoDataFrame(data=[a_node,b_node]) + + def segment(direction, road): + segment = {'ovm_id': '8b9d0e128cd9fff-163FF6797FC40661', 'connectors': ['8f9d0e128cd9709-167FF64A37F1BFFB', '8f9d0e128cd98d6-15FFF68E65613FDF'], 'direction': direction, + 'link_type': 'secondary', 'name': 'Shute Harbour Road', 'speed': '{"maxSpeed":[70,"km/h"]}', 'road': road, + 'geometry': shapely.LineString([(148.7245987, -20.2747175), (148.7246504, -20.2747531), (148.724688, -20.274802), (148.7247077, -20.2748593), (148.7247078, -20.2749195)])} + return segment + + dataframes = [gpd.GeoDataFrame(), node_df] + o = OVMBuilder(ovm_download=dataframes, project_path=self.pth, project=self.project) + + with tempfile.TemporaryDirectory() as output_dir: + + # for lane_type in [no_info, simple, lanes_3, highway, lane_ends]: + # print(gpd.GeoDataFrame(segment(lane_type, road(lane_type)))['connectors']) + # print(node_df) + + # assert type(road(lane_type)) == str + # assert type(o.split_connectors(segment(lane_type, road(lane_type)))) == gpd.GeoDataFrame + + dataframes = [gpd.GeoDataFrame(segment(no_info, road(no_info))), node_df] + o = OVMBuilder(ovm_download=dataframes, project_path=self.pth, project=self.project) + # o.__worksetup() + o.create_node_ids(node_df) + # gdf_no_info = o.split_connectors(segment(no_info, road(no_info))) + gdf_no_info = o.formatting(dataframes[0], dataframes[1], output_dir) + print(gdf_no_info.columns) + + assert gdf_no_info['direction'][0] == 0 + assert gdf_no_info['lanes_ab'][0] == 1 + assert gdf_no_info['lanes_ba'][0] == 1 + print('gdf_no_info test: passed') + + gdf_simple = o.split_connectors(segment(simple, road(simple))) + + assert len(simple) == 2 + assert gdf_simple['direction'][0] == 0 + assert gdf_simple['lanes_ab'][0] == 1 + assert gdf_simple['lanes_ab'][0] == 1 + print('gdf_simple test: passed') + + gdf_lanes_3 = o.split_connectors(segment(lanes_3, road(lanes_3))) + + assert len(lanes_3) == 3 + assert gdf_lanes_3['direction'][0] == 1 + assert gdf_lanes_3['lanes_ab'][0] == 3 + assert gdf_lanes_3['lanes_ba'][0] == None + print('gdf_lanes_3 test: passed') + + gdf_highway = o.split_connectors(segment(highway, road(highway))) + + assert len(highway) == 8 + assert gdf_highway['direction'][0] == 0 + assert gdf_highway['lanes_ab'][0] == 4 + assert gdf_highway['lanes_ba'][0] == 4 + print('gdf_highway test: passed') + + gdf_lane_ends = o.split_connectors(segment(lane_ends, road(lane_ends))) + + assert len(lane_ends) == 2 + assert len(lane_ends[0][0]['value']) == 3 + assert len(lane_ends[1][0]['value']) == 2 + assert gdf_lane_ends['direction'][0] == 0 + assert gdf_lane_ends['lanes_ab'][0] == 2 + assert gdf_lane_ends['lanes_ba'][0] == 1 + print('gdf_lane_ends test: passed') + + gdf_lane_begins = o.split_connectors(segment(lane_begins, road(lane_begins))) + + assert len(lane_begins) == 2 + assert len(lane_begins[0][0]['value']) == 2 + assert len(lane_begins[1][0]['value']) == 3 + assert gdf_lane_begins['direction'][0] == 0 + assert gdf_lane_begins['lanes_ab'][0] == 2 + assert gdf_lane_begins['lanes_ba'][0] == 1 + print('gdf_lane_begins test: passed') + + + gdf_lane_merge_twice = o.split_connectors(segment(lane_merge_twice, road(lane_merge_twice))) + + assert len(lane_merge_twice) == 3 + assert len(lane_merge_twice[0][0]['value']) == 4 + assert len(lane_merge_twice[1][0]['value']) == 3 + assert len(lane_merge_twice[2][0]['value']) == 2 + assert gdf_lane_merge_twice['direction'][0] == 0 + assert gdf_lane_merge_twice['lanes_ab'][0] == 2 + assert gdf_lane_merge_twice['lanes_ba'][0] == 1 + print('gdf_lane_merge_twice test: passed') + + gdf_equal_dis = o.split_connectors(segment(equal_dis, road(equal_dis))) + + assert len(equal_dis) == 2 + assert len(equal_dis[0][0]['value']) == 3 + assert len(equal_dis[1][0]['value']) == 2 + assert gdf_equal_dis['direction'][0] == 0 + assert gdf_equal_dis['lanes_ab'][0] == 2 + assert gdf_equal_dis['lanes_ba'][0] == 1 + print('gdf_lane_ends test: passed') \ No newline at end of file diff --git a/tests/data/overture/theme=transportation/type=connector/airlie_beach_transportation_connector.parquet b/tests/data/overture/theme=transportation/type=connector/airlie_beach_transportation_connector.parquet new file mode 100644 index 0000000000000000000000000000000000000000..dd81d19d429f2cf4477fc4ce6266b8388ac214e2 GIT binary patch literal 57352 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 giWKpbbtJ@-vqZR~7&m>@uIu-| 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 z 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 Date: Thu, 1 Feb 2024 15:54:22 +1000 Subject: [PATCH 2/8] . --- .vscode/settings.json | 13 ------------- aequilibrae/parameters.yml | 6 ------ 2 files changed, 19 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index dce22a3bc..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "python.testing.pytestArgs": [], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "julia.environmentPath": "c:\\Users\\penny\\git\\Aequilibrae", - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test*.py" - ] -} \ No newline at end of file diff --git a/aequilibrae/parameters.yml b/aequilibrae/parameters.yml index 6d48e2f51..de2fc937e 100644 --- a/aequilibrae/parameters.yml +++ b/aequilibrae/parameters.yml @@ -262,8 +262,6 @@ network: - pedestrian - track - unclassified - mode_filter: - bicycle: 'no' unknown_tags: true car: link_types: @@ -276,8 +274,6 @@ network: - residential - livingStreet - parkingAisle - mode_filter: - motor_vehicle: 'no' unknown_tags: true transit: link_types: @@ -299,8 +295,6 @@ network: - track - bridleway - unclassified - mode_filter: - pedestrian: 'no' unknown_tags: true gmns: critical_dist: 2 From fc361c576643a8803cbbccb730dc98773da8fdd0 Mon Sep 17 00:00:00 2001 From: Jamie Cook Date: Thu, 1 Feb 2024 16:26:24 +1000 Subject: [PATCH 3/8] cleaning --- aequilibrae/project/network/messing_around.py | 797 ------------------ aequilibrae/project/network/ovm_downloader.py | 62 +- .../examples/creating_models/from_osm.py | 2 - .../examples/creating_models/from_ovm.py | 17 + .../project/ovm/setup_test_data.ipynb | 112 +-- .../type=segment/ovm.parquet | Bin 51967 -> 0 bytes 6 files changed, 58 insertions(+), 932 deletions(-) delete mode 100644 aequilibrae/project/network/messing_around.py delete mode 100755 tests/data/ovm/type=transportation/type=segment/ovm.parquet diff --git a/aequilibrae/project/network/messing_around.py b/aequilibrae/project/network/messing_around.py deleted file mode 100644 index 243ecaa2f..000000000 --- a/aequilibrae/project/network/messing_around.py +++ /dev/null @@ -1,797 +0,0 @@ -# %% -import importlib.util as iutil -import math -from sqlite3 import Connection as sqlc -from typing import Dict - -import numpy as np -import pandas as pd -import shapely.wkb -import shapely.wkt -from shapely.geometry import Polygon -from shapely.ops import unary_union - -from aequilibrae.context import get_logger -from aequilibrae.parameters import Parameters -from aequilibrae.project.network import OSMDownloader -from aequilibrae.project.network.gmns_builder import GMNSBuilder -from aequilibrae.project.network.gmns_exporter import GMNSExporter -from aequilibrae.project.network.haversine import haversine -from aequilibrae.project.network.link_types import LinkTypes -from aequilibrae.project.network.links import Links -from aequilibrae.project.network.modes import Modes -from aequilibrae.project.network.nodes import Nodes -from aequilibrae.project.network.osm_builder import OSMBuilder -from aequilibrae.project.network.osm_utils.place_getter import placegetter -from aequilibrae.project.project_creation import req_link_flds, req_node_flds, protected_fields -from aequilibrae.utils import WorkerThread - -spec = iutil.find_spec("PyQt5") -pyqt = spec is not None -if pyqt: - from PyQt5.QtCore import pyqtSignal as SIGNAL - - -class Network(WorkerThread): - """ - Network class. Member of an AequilibraE Project - """ - - if pyqt: - netsignal = SIGNAL(object) - - req_link_flds = req_link_flds - req_node_flds = req_node_flds - protected_fields = protected_fields - link_types: LinkTypes = None - - def __init__(self, project) -> None: - from aequilibrae.paths import Graph - - WorkerThread.__init__(self, None) - self.conn = project.conn # type: sqlc - self.source = project.source # type: sqlc - self.graphs = {} # type: Dict[Graph] - self.project = project - self.logger = project.logger - self.modes = Modes(self) - self.link_types = LinkTypes(self) - self.links = Links(self) - self.nodes = Nodes(self) - - def skimmable_fields(self): - """ - Returns a list of all fields that can be skimmed - - :Returns: - :obj:`list`: List of all fields that can be skimmed - """ - curr = self.conn.cursor() - curr.execute("PRAGMA table_info(links);") - field_names = curr.fetchall() - ignore_fields = ["ogc_fid", "geometry"] + self.req_link_flds - - skimmable = [ - "INT", - "INTEGER", - "TINYINT", - "SMALLINT", - "MEDIUMINT", - "BIGINT", - "UNSIGNED BIG INT", - "INT2", - "INT8", - "REAL", - "DOUBLE", - "DOUBLE PRECISION", - "FLOAT", - "DECIMAL", - "NUMERIC", - ] - all_fields = [] - - for f in field_names: - if f[1] in ignore_fields: - continue - for i in skimmable: - if i in f[2].upper(): - all_fields.append(f[1]) - break - - all_fields.append("distance") - real_fields = [] - for f in all_fields: - if f[-2:] == "ab": - if f[:-2] + "ba" in all_fields: - real_fields.append(f[:-3]) - elif f[-3:] != "_ba": - real_fields.append(f) - - return real_fields - - def list_modes(self): - """ - Returns a list of all the modes in this model - - :Returns: - :obj:`list`: List of all modes - """ - curr = self.conn.cursor() - curr.execute("""select mode_id from modes""") - return [x[0] for x in curr.fetchall()] - - def create_from_osm( - self, - west: float = None, - south: float = None, - east: float = None, - north: float = None, - place_name: str = None, - modes=["car", "transit", "bicycle", "walk"], - ) -> None: - """ - Downloads the network from Open-Street Maps - - :Arguments: - **west** (:obj:`float`, Optional): West most coordinate of the download bounding box - - **south** (:obj:`float`, Optional): South most coordinate of the download bounding box - - **east** (:obj:`float`, Optional): East most coordinate of the download bounding box - - **place_name** (:obj:`str`, Optional): If not downloading with East-West-North-South boundingbox, this is - required - - **modes** (:obj:`list`, Optional): List of all modes to be downloaded. Defaults to the modes in the parameter - file - - .. code-block:: python - - >>> from aequilibrae import Project - - >>> p = Project() - >>> p.new("/tmp/new_project") - - # We now choose a different overpass endpoint (say a deployment in your local network) - >>> par = Parameters() - >>> par.parameters['osm']['overpass_endpoint'] = "http://192.168.1.234:5678/api" - - # Because we have our own server, we can set a bigger area for download (in M2) - >>> par.parameters['osm']['max_query_area_size'] = 10000000000 - - # And have no pause between successive queries - >>> par.parameters['osm']['sleeptime'] = 0 - - # Save the parameters to disk - >>> par.write_back() - - # Now we can import the network for any place we want - # p.network.create_from_osm(place_name="my_beautiful_hometown") - - >>> p.close() - """ - - if self.count_links() > 0: - raise FileExistsError("You can only import an OSM network into a brand new model file") - - curr = self.conn.cursor() - curr.execute("""ALTER TABLE links ADD COLUMN osm_id integer""") - curr.execute("""ALTER TABLE nodes ADD COLUMN osm_id integer""") - self.conn.commit() - - if isinstance(modes, (tuple, list)): - modes = list(modes) - elif isinstance(modes, str): - modes = [modes] - else: - raise ValueError("'modes' needs to be string or list/tuple of string") - - if place_name is None: - if min(east, west) < -180 or max(east, west) > 180 or min(north, south) < -90 or max(north, south) > 90: - raise ValueError("Coordinates out of bounds") - bbox = [west, south, east, north] - else: - bbox, report = placegetter(place_name) - west, south, east, north = bbox - if bbox is None: - msg = f'We could not find a reference for place name "{place_name}"' - self.logger.warning(msg) - return - for i in report: - if "PLACE FOUND" in i: - self.logger.info(i) - - # Need to compute the size of the bounding box to not exceed it too much - height = haversine((east + west) / 2, south, (east + west) / 2, north) - width = haversine(east, (north + south) / 2, west, (north + south) / 2) - area = height * width - - par = Parameters().parameters["osm"] - max_query_area_size = par["max_query_area_size"] - - if area < max_query_area_size: - polygons = [bbox] - else: - polygons = [] - parts = math.ceil(area / max_query_area_size) - horizontal = math.ceil(math.sqrt(parts)) - vertical = math.ceil(parts / horizontal) - dx = (east - west) / horizontal - dy = (north - south) / vertical - for i in range(horizontal): - xmin = max(-180, west + i * dx) - xmax = min(180, west + (i + 1) * dx) - for j in range(vertical): - ymin = max(-90, south + j * dy) - ymax = min(90, south + (j + 1) * dy) - box = [xmin, ymin, xmax, ymax] - polygons.append(box) - self.logger.info("Downloading data") - self.downloader = OSMDownloader(polygons, modes, logger=self.logger) - if pyqt: - self.downloader.downloading.connect(self.signal_handler) - - self.downloader.doWork() - - self.logger.info("Building Network") - self.builder = OSMBuilder(self.downloader.json, self.source, project=self.project) - - if pyqt: - self.builder.building.connect(self.signal_handler) - self.builder.doWork() - - self.logger.info("Network built successfully") - - def create_from_gmns( - self, - link_file_path: str, - node_file_path: str, - use_group_path: str = None, - geometry_path: str = None, - srid: int = 4326, - ) -> None: - """ - Creates AequilibraE model from links and nodes in GMNS format. - - :Arguments: - **link_file_path** (:obj:`str`): Path to a links csv file in GMNS format - - **node_file_path** (:obj:`str`): Path to a nodes csv file in GMNS format - - **use_group_path** (:obj:`str`, Optional): Path to a csv table containing groupings of uses. This helps AequilibraE - know when a GMNS use is actually a group of other GMNS uses - - **geometry_path** (:obj:`str`, Optional): Path to a csv file containing geometry information for a line object, if not - specified in the link table - - **srid** (:obj:`int`, Optional): Spatial Reference ID in which the GMNS geometries were created - """ - - gmns_builder = GMNSBuilder(self, link_file_path, node_file_path, use_group_path, geometry_path, srid) - gmns_builder.doWork() - - self.logger.info("Network built successfully") - - def export_to_gmns(self, path: str): - """ - Exports AequilibraE network to csv files in GMNS format. - - :Arguments: - **path** (:obj:`str`): Output folder path. - """ - - gmns_exporter = GMNSExporter(self, path) - gmns_exporter.doWork() - - self.logger.info("Network exported successfully") - - def signal_handler(self, val): - if pyqt: - self.netsignal.emit(val) - - def build_graphs(self, fields: list = None, modes: list = None) -> None: - """Builds graphs for all modes currently available in the model - - When called, it overwrites all graphs previously created and stored in the networks' - dictionary of graphs - - :Arguments: - **fields** (:obj:`list`, optional): When working with very large graphs with large number of fields in the - database, it may be useful to specify which fields to use - **modes** (:obj:`list`, optional): When working with very large graphs with large number of fields in the - database, it may be useful to generate only those we need - - To use the *fields* parameter, a minimalistic option is the following - - .. code-block:: python - - >>> from aequilibrae import Project - - >>> p = Project.from_path("/tmp/test_project") - >>> fields = ['distance'] - >>> p.network.build_graphs(fields, modes = ['c', 'w']) - - """ - from aequilibrae.paths import Graph - - curr = self.conn.cursor() - - if fields is None: - curr.execute("PRAGMA table_info(links);") - field_names = curr.fetchall() - - ignore_fields = ["ogc_fid", "geometry"] - all_fields = [f[1] for f in field_names if f[1] not in ignore_fields] - else: - fields.extend(["link_id", "a_node", "b_node", "direction", "modes"]) - all_fields = list(set(fields)) - - if modes is None: - modes = curr.execute("select mode_id from modes;").fetchall() - modes = [m[0] for m in modes] - elif isinstance(modes, str): - modes = [modes] - - sql = f"select {','.join(all_fields)} from links" - - df = pd.read_sql(sql, self.conn).fillna(value=np.nan) - valid_fields = list(df.select_dtypes(np.number).columns) + ["modes"] - curr.execute("select node_id from nodes where is_centroid=1 order by node_id;") - centroids = np.array([i[0] for i in curr.fetchall()], np.uint32) - - data = df[valid_fields] - for m in modes: - net = pd.DataFrame(data, copy=True) - net.loc[~net.modes.str.contains(m), "b_node"] = net.loc[~net.modes.str.contains(m), "a_node"] - g = Graph() - g.mode = m - g.network = net - if centroids.shape[0]: - g.prepare_graph(centroids) - g.set_blocked_centroid_flows(True) - else: - get_logger().warning("Your graph has no centroids") - self.graphs[m] = g - - def set_time_field(self, time_field: str) -> None: - """ - Set the time field for all graphs built in the model - - :Arguments: - **time_field** (:obj:`str`): Network field with travel time information - """ - for m, g in self.graphs.items(): - if time_field not in list(g.graph.columns): - raise ValueError(f"{time_field} not available. Check if you have NULL values in the database") - g.free_flow_time = time_field - g.set_graph(time_field) - self.graphs[m] = g - - def count_links(self) -> int: - """ - Returns the number of links in the model - - :Returns: - :obj:`int`: Number of links - """ - return self.__count_items("link_id", "links", "link_id>=0") - - def count_centroids(self) -> int: - """ - Returns the number of centroids in the model - - :Returns: - :obj:`int`: Number of centroids - """ - return self.__count_items("node_id", "nodes", "is_centroid=1") - - def count_nodes(self) -> int: - """ - Returns the number of nodes in the model - - :Returns: - :obj:`int`: Number of nodes - """ - return self.__count_items("node_id", "nodes", "node_id>=0") - - def extent(self): - """Queries the extent of the network included in the model - - :Returns: - **model extent** (:obj:`Polygon`): Shapely polygon with the bounding box of the model network. - """ - curr = self.conn.cursor() - curr.execute('Select ST_asBinary(GetLayerExtent("Links"))') - poly = shapely.wkb.loads(curr.fetchone()[0]) - return poly - - def convex_hull(self) -> Polygon: - """Queries the model for the convex hull of the entire network - - :Returns: - **model coverage** (:obj:`Polygon`): Shapely (Multi)polygon of the model network. - """ - curr = self.conn.cursor() - curr.execute('Select ST_asBinary("geometry") from Links where ST_Length("geometry") > 0;') - links = [shapely.wkb.loads(x[0]) for x in curr.fetchall()] - return unary_union(links).convex_hull - - def refresh_connection(self): - """Opens a new database connection to avoid thread conflict""" - self.conn = self.project.connect() - - def __count_items(self, field: str, table: str, condition: str) -> int: - c = self.conn.execute(f"select count({field}) from {table} where {condition};").fetchone()[0] - return c - -# %% -# Imports -from uuid import uuid4 -from tempfile import gettempdir -from os.path import join -from aequilibrae import Project -import folium -# sphinx_gallery_thumbnail_path = 'images/nauru.png' - -# %% -# We create an empty project on an arbitrary folder -fldr = join(gettempdir(), uuid4().hex) -project = Project() -project.new(fldr) - -# %% -project.network.create_from_osm(place_name="Airlie Beach") - - -# %% -links = project.network.links.data -links - -# %% -project.network.nodes.data - -#%% -project.network.count_links() - -# %% -project.network.count_nodes() - -# %% -curr = project.network.conn.cursor() -project.network.conn.commit() - -# %% -modes = Modes(project.network) -modes=["car", "transit", "bicycle", "walk"] -if isinstance(modes, (tuple, list)): - modes = list(modes) -elif isinstance(modes, str): - modes = [modes] -else: - raise ValueError("modes needs to be string or list/tuple of string") -modes - -# %% -par = Parameters().parameters["osm"] -max_query_area_size = par["max_query_area_size"] -par - -# %% -Parameters().__dict__ - -# %% -Parameters().parameters["network"] - -# %% -place_name = "Nauru" - -# %% -north: float = None -east: float = None -south: float = None -west: float = None - -if place_name is None: - if min(east, west) < -180 or max(east, west) > 180 or min(north, south) < -90 or max(north, south) > 90: - raise ValueError("Coordinates out of bounds") - bbox = [west, south, east, north] -else: - bbox, report = placegetter(place_name) - west, south, east, north = bbox - if bbox is None: - msg = f'We could not find a reference for place name "{place_name}"' - project.network.logger.warning(msg) - - for i in report: - if "PLACE FOUND" in i: - project.network.logger.info(i) - -# %% -bbox - -# %% -height = haversine((east + west) / 2, south, (east + west) / 2, north) -width = haversine(east, (north + south) / 2, west, (north + south) / 2) -area = height * width -area - -# %% -area < max_query_area_size - -# %% -polygons = [] -parts = math.ceil(area / max_query_area_size) -horizontal = math.ceil(math.sqrt(parts)) -vertical = math.ceil(parts / horizontal) -dx = (east - west) / horizontal -dy = (north - south) / vertical - -# %% -parts -# %% -horizontal -# %% -vertical -# %% -dx -# %% -dy -# %% -for i in range(horizontal): - xmin = max(-180, west + i * dx) - xmax = min(180, west + (i + 1) * dx) - for j in range(vertical): - ymin = max(-90, south + j * dy) - ymax = min(90, south + (j + 1) * dy) - box = [xmin, ymin, xmax, ymax] - polygons.append(box) - -polygons -# %% -ymax -# %% -ymin -# %% -xmax -# %% -xmin -# %% -project.network.downloader = OSMDownloader(polygons, modes, logger= project.network.logger) -project.network.downloader - -# %% -print(project.network.downloader.json) -print(project.network.downloader.report) -print(polygons) -print(project.network.logger) - -# %% -if pyqt: - project.network.builder.building.connect(project.network.signal_handler) - -# project.network.builder.doWork() -# we will come back to the builder - -# %% -project.network.downloader.doWork() -project.network.downloader.json - -# %% - -# looking through downloader's doWork() -import logging -import time -import re -import requests -from aequilibrae.project.network.osm_utils.osm_params import http_headers, memory -from aequilibrae.parameters import Parameters -from aequilibrae.context import get_logger -import gc -import importlib.util as iutil -from aequilibrae.utils import WorkerThread - -spec = iutil.find_spec("PyQt5") -pyqt = spec is not None -if pyqt: - from PyQt5.QtCore import pyqtSignal - -# %% -infrastructure = 'way["highway"]' -query_template = ( - "{memory}[out:json][timeout:{timeout}];({infrastructure}{filters}({south:.6f},{west:.6f}," - "{north:.6f},{east:.6f});>;);out;" - ) - -# %% -query_template - -# %% -project.__getstate__() - -# %% -WorkerThread.__init__(project.network.downloader, None) -project.network.downloader.logger = get_logger() -project.network.downloader.polygons = polygons -project.network.downloader.filter = project.network.downloader.get_osm_filter(modes) -project.network.downloader.report = [] -project.network.downloader.json = [] -par = Parameters().parameters["osm"] -project.network.downloader.overpass_endpoint = par["overpass_endpoint"] -project.network.downloader.timeout = par["timeout"] -project.network.downloader.sleeptime = par["sleeptime"] - -# %% -project.network.downloader.downloading.emit(["maxValue", len(project.network.downloader.polygons)]) -project.network.downloader.downloading.emit(["Value", 0]) - -# %% -m = "" -if memory > 0: - m = f"[maxsize: {memory}]" -memory -# %% -query_str = query_template.format( - north=north, - south=south, - east=east, - west=west, - infrastructure=infrastructure, - filters=project.network.downloader.filter, - timeout=project.network.downloader.timeout, - memory=m, - ) -query_str -# %% -project.network.downloader.overpass_request(data={"data": query_str}, timeout=project.network.downloader.timeout) - -# %% -json = project.network.downloader.overpass_request(data={"data": query_str}, timeout=project.network.downloader.timeout) -json -# %% -if json["elements"]: - project.network.downloader.json.extend(json["elements"]) -del json -project.network.downloader.json - -# %% -gc.collect() - -# %% -for counter, poly in enumerate(project.network.downloader.polygons): - msg = f"Downloading polygon {counter + 1} of {len(project.network.downloader.polygons)}" - project.network.downloader.logger.debug(msg) - project.network.downloader.downloading.emit(["Value", counter]) - project.network.downloader.downloading.emit(["text", msg]) - west, south, east, north = poly - query_str = query_template.format( - north=north, - south=south, - east=east, - west=west, - infrastructure=infrastructure, - filters=project.network.downloader.filter, - timeout=project.network.downloader.timeout, - memory=m, - ) - json = project.network.downloader.overpass_request(data={"data": query_str}, timeout=project.network.downloader.timeout) - if json["elements"]: - project.network.downloader.json.extend(json["elements"]) - del json - gc.collect() -project.network.downloader.downloading.emit(["Value", len(project.network.downloader.polygons)]) -project.network.downloader.downloading.emit(["FinishedDownloading", 0]) -project.network.downloader.json - -# %% -project.network.logger.info("Downloading data") -project.network.downloader = OSMDownloader(polygons, modes, logger=project.network.logger) -print(project.network.downloader.json) -project.network.downloader.doWork() -project.network.downloader.json - -# %% -project.network.logger.info("Building Network") -project.network.builder = OSMBuilder(project.network.downloader.json, project.network.source, project=project.network.project) -if pyqt: - project.network.builder.building.connect(project.network.signal_handler) - -# project.network.builder.doWork() -# can't access doWork() in this file, so we will go through each step of the function separately here -project.network.builder.nodes - -# %% - -# looking through builder's doWork() -import gc -import importlib.util as iutil -import sqlite3 -import string -from typing import List - -import numpy as np -import pandas as pd - -from aequilibrae.context import get_active_project -from aequilibrae.parameters import Parameters -from aequilibrae.project.network.link_types import LinkTypes -from aequilibrae.utils.spatialite_utils import connect_spatialite -from aequilibrae.project.network.haversine import haversine -from aequilibrae.utils import WorkerThread - -spec = iutil.find_spec("PyQt5") -pyqt = spec is not None -if pyqt: - from PyQt5.QtCore import pyqtSignal - -spec = iutil.find_spec("qgis") -isqgis = spec is not None -if isqgis: - import qgis - -# %% -WorkerThread.__init__(project.network.builder, None) -project.network.builder.project = project or get_active_project() -project.network.builder.logger = project.network.builder.project.logger -project.network.builder.conn = None -project.network.builder.__link_types = None # type: LinkTypes -project.network.builder.report = [] -project.network.builder.__model_link_types = [] -project.network.builder.__model_link_type_ids = [] -project.network.builder.__link_type_quick_reference = {} -project.network.builder.nodes = {} -project.network.builder.node_df = [] -project.network.builder.links = {} -project.network.builder.insert_qry = """INSERT INTO {} ({}, geometry) VALUES({}, GeomFromText(?, 4326))""" -# %% -project.network.builder.conn = connect_spatialite(project.network.builder.path) -print(project.network.builder.conn) -project.network.builder.curr = project.network.builder.conn.cursor() -print(project.network.builder.curr) - -# project.network.builder.__worksetup() -# can't access so will go through each step - -# %% -project.network.builder.__link_types = project.network.builder.project.network.link_types -lts = project.network.builder.__link_types.all_types() -for lt_id, lt in lts.items(): - project.network.builder.__model_link_types.append(lt.link_type) - project.network.builder.__model_link_type_ids.append(lt_id) -# %% -print(project.network.builder.__model_link_types) -print(project.network.builder.__model_link_type_ids) -lts - -# %% - -# back to doWork -node_count = project.network.builder.data_structures() -node_count - -# %% - -# project.network.builder.importing_links(node_count) -# this would have been the next step in doWork but again we don't have access to this function -# in this particual case so we will go step by step -node_ids = {} - -vars = {} -vars["link_id"] = 1 -table = "links" -# fields = project.network.builder.get_link_fields() -# same again no access, so step by step -p = Parameters() -fields = p.parameters["network"]["links"]["fields"] -owf = [list(x.keys())[0] for x in fields["one-way"]] - -twf1 = ["{}_ab".format(list(x.keys())[0]) for x in fields["two-way"]] -twf2 = ["{}_ba".format(list(x.keys())[0]) for x in fields["two-way"]] - -return_get_link_fields = owf + twf1 + twf2 + ["osm_id"] -print(fields) -print(owf) -print(twf1) -print(twf2) -print(return_get_link_fields) - -# %% diff --git a/aequilibrae/project/network/ovm_downloader.py b/aequilibrae/project/network/ovm_downloader.py index 34ee489c1..1509ec57c 100644 --- a/aequilibrae/project/network/ovm_downloader.py +++ b/aequilibrae/project/network/ovm_downloader.py @@ -173,8 +173,37 @@ def downloadTransportation(self, bbox: list, data_source: Union[str, Path], outp return gdf_link, gdf_node - def download_test_data(self, data_source: Union[str, Path]): - '''This method only used to seed/bootstrap a local copy of a small test data set''' + def get_ovm_filter(self, modes: list) -> str: + """ + loosely adapted from http://www.github.com/gboeing/osmnx + """ + + p = Parameters().parameters["network"]["ovm"] + all_tags = p["all_link_types"] + + p = p["modes"] + all_modes = list(p.keys()) + + tags_to_keep = [] + for m in modes: + if m not in all_modes: + raise ValueError(f"Mode {m} not listed in the parameters file") + tags_to_keep += p[m]["link_types"] + tags_to_keep = list(set(tags_to_keep)) + + # Default to remove + service = '["service"!~"parking|parking_aisle|driveway|private|emergency_access"]' + access = '["access"!~"private"]' + + filtered = [x for x in all_tags if x not in tags_to_keep] + filtered = "|".join(filtered) + + filter = f'["area"!~"yes"]["highway"!~"{filtered}"]{service}{access}' + + return filter + + def _download_test_data(self, data_source: Union[str, Path]): + '''This method only used to seed/bootstrap a local copy of a small test data set which should be commited to version control''' airlie_bbox = [148.7077, -20.2780, 148.7324, -20.2621 ] # brisbane_bbox = [153.1771, -27.6851, 153.2018, -27.6703] data_source = data_source.replace("\\", "/") @@ -204,32 +233,3 @@ def download_test_data(self, data_source: Union[str, Path]): gdf.to_parquet(Path(pth1)) # return gdf - def get_ovm_filter(self, modes: list) -> str: - """ - loosely adapted from http://www.github.com/gboeing/osmnx - """ - - p = Parameters().parameters["network"]["ovm"] - all_tags = p["all_link_types"] - - p = p["modes"] - all_modes = list(p.keys()) - - tags_to_keep = [] - for m in modes: - if m not in all_modes: - raise ValueError(f"Mode {m} not listed in the parameters file") - tags_to_keep += p[m]["link_types"] - tags_to_keep = list(set(tags_to_keep)) - - # Default to remove - service = '["service"!~"parking|parking_aisle|driveway|private|emergency_access"]' - access = '["access"!~"private"]' - - filtered = [x for x in all_tags if x not in tags_to_keep] - filtered = "|".join(filtered) - - filter = f'["area"!~"yes"]["highway"!~"{filtered}"]{service}{access}' - - return filter - \ No newline at end of file diff --git a/docs/source/examples/creating_models/from_osm.py b/docs/source/examples/creating_models/from_osm.py index fbbdf8758..93d634349 100644 --- a/docs/source/examples/creating_models/from_osm.py +++ b/docs/source/examples/creating_models/from_osm.py @@ -55,8 +55,6 @@ curr = project.conn.cursor() curr.execute("select avg(xmin), avg(ymin) from idx_links_geometry") long, lat = curr.fetchone() -print(long) -print(lat) # %% map_osm = folium.Map(location=[lat, long], zoom_start=14) diff --git a/docs/source/examples/creating_models/from_ovm.py b/docs/source/examples/creating_models/from_ovm.py index 0675e1b77..97cda898a 100644 --- a/docs/source/examples/creating_models/from_ovm.py +++ b/docs/source/examples/creating_models/from_ovm.py @@ -1,3 +1,20 @@ +# --- +# jupyter: +# jupytext: +# cell_metadata_filter: -all +# custom_cell_magics: kql +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.2 +# kernelspec: +# display_name: venv +# language: python +# name: python3 +# --- + +# %% """ Project from Overture Maps ============================= diff --git a/tests/aequilibrae/project/ovm/setup_test_data.ipynb b/tests/aequilibrae/project/ovm/setup_test_data.ipynb index 840cd8f81..2a56d5a73 100644 --- a/tests/aequilibrae/project/ovm/setup_test_data.ipynb +++ b/tests/aequilibrae/project/ovm/setup_test_data.ipynb @@ -11,126 +11,34 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "import os, sys\n", "from pathlib import Path\n", - "import pandas as pd\n", - "import geopandas as gpd\n", - "import shapely\n", - "from aequilibrae.project.network.ovm_downloader import OVMDownloader\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "C:\\Users\\penny\\git\\Aequilibrae\n", - "['c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\tests\\\\aequilibrae\\\\project\\\\ovm', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311\\\\python311.zip', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311\\\\DLLs', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311\\\\Lib', 'C:\\\\Users\\\\penny\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python311', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv', '', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages', 'C:\\\\Users\\\\penny\\\\git\\\\Aequilibrae', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages\\\\win32', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages\\\\win32\\\\lib', 'c:\\\\Users\\\\penny\\\\git\\\\Aequilibrae\\\\.venv\\\\Lib\\\\site-packages\\\\Pythonwin']\n", - "C:\\Users\\penny\\git\\Aequilibrae\\theme=transportation\\type=segment\n" - ] - }, - { - "ename": "AttributeError", - "evalue": "'OVMDownloader' object has no attribute 'replace'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[3], line 19\u001b[0m\n\u001b[0;32m 14\u001b[0m \u001b[38;5;28mprint\u001b[39m(test_data_dir)\n\u001b[0;32m 17\u001b[0m ovm_downloader_instance \u001b[38;5;241m=\u001b[39m OVMDownloader([\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcar\u001b[39m\u001b[38;5;124m\"\u001b[39m],test_data_dir)\n\u001b[1;32m---> 19\u001b[0m \u001b[43movm_downloader_instance\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdownload_test_data\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mE:/theme=transportation/type=segment\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32m~\\git\\Aequilibrae\\aequilibrae\\project\\network\\ovm_downloader.py:116\u001b[0m, in \u001b[0;36mdownload_test_data\u001b[1;34m(data_source, test_data_location)\u001b[0m\n\u001b[0;32m 114\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 115\u001b[0m c\u001b[38;5;241m.\u001b[39mexecute(sql)\n\u001b[1;32m--> 116\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 117\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mAn error occurred: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n", - "\u001b[1;31mAttributeError\u001b[0m: 'OVMDownloader' object has no attribute 'replace'" - ] - } - ], - "source": [ - "\n", "aeq_dir = str(Path('../../../../').resolve())\n", - "print(aeq_dir)\n", - "print(sys.path)\n", "if aeq_dir not in sys.path:\n", " sys.path.append(aeq_dir)\n", "\n", - "\n", - "test_data_dir = Path(aeq_dir) / 'theme=transportation' / 'type=segment' \n", - "print(test_data_dir)\n", - "\n", - "\n", - "ovm_downloader_instance = OVMDownloader([\"car\"],test_data_dir)\n", - "\n", - "ovm_downloader_instance.download_test_data('E:/theme=transportation/type=segment')\n" + "import pandas as pd\n", + "import geopandas as gpd\n", + "import shapely\n", + "from aequilibrae.project.network.ovm_downloader import OVMDownloader\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, - "outputs": [ - { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/ovm.parquet'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[7], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m os\u001b[38;5;241m.\u001b[39mgetcwd()\n\u001b[1;32m----> 2\u001b[0m df \u001b[38;5;241m=\u001b[39m \u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m../data/ovm/type=transportation/type=segment/ovm.parquet\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 3\u001b[0m df\n\u001b[0;32m 4\u001b[0m \u001b[38;5;66;03m# gdf = gpd.GeoDOataFrame(data=df, geometry=df.geometry.apply(shapely.wkb.loads))\u001b[39;00m\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:670\u001b[0m, in \u001b[0;36mread_parquet\u001b[1;34m(path, engine, columns, storage_options, use_nullable_dtypes, dtype_backend, filesystem, filters, **kwargs)\u001b[0m\n\u001b[0;32m 667\u001b[0m use_nullable_dtypes \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[0;32m 668\u001b[0m check_dtype_backend(dtype_backend)\n\u001b[1;32m--> 670\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mimpl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 671\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 672\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 673\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 674\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 675\u001b[0m \u001b[43m \u001b[49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 676\u001b[0m \u001b[43m \u001b[49m\u001b[43mdtype_backend\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdtype_backend\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 677\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 678\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 679\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:265\u001b[0m, in \u001b[0;36mPyArrowImpl.read\u001b[1;34m(self, path, columns, filters, use_nullable_dtypes, dtype_backend, storage_options, filesystem, **kwargs)\u001b[0m\n\u001b[0;32m 262\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m manager \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124marray\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 263\u001b[0m to_pandas_kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msplit_blocks\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m \u001b[38;5;66;03m# type: ignore[assignment]\u001b[39;00m\n\u001b[1;32m--> 265\u001b[0m path_or_handle, handles, filesystem \u001b[38;5;241m=\u001b[39m \u001b[43m_get_path_or_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 266\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 267\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 268\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 269\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 270\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 271\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 272\u001b[0m pa_table \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapi\u001b[38;5;241m.\u001b[39mparquet\u001b[38;5;241m.\u001b[39mread_table(\n\u001b[0;32m 273\u001b[0m path_or_handle,\n\u001b[0;32m 274\u001b[0m columns\u001b[38;5;241m=\u001b[39mcolumns,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 277\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 278\u001b[0m )\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:139\u001b[0m, in \u001b[0;36m_get_path_or_handle\u001b[1;34m(path, fs, storage_options, mode, is_dir)\u001b[0m\n\u001b[0;32m 129\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[0;32m 131\u001b[0m \u001b[38;5;129;01mnot\u001b[39;00m fs\n\u001b[0;32m 132\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_dir\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 137\u001b[0m \u001b[38;5;66;03m# fsspec resources can also point to directories\u001b[39;00m\n\u001b[0;32m 138\u001b[0m \u001b[38;5;66;03m# this branch is used for example when reading from non-fsspec URLs\u001b[39;00m\n\u001b[1;32m--> 139\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[43mget_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 140\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_handle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mis_text\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\n\u001b[0;32m 141\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 142\u001b[0m fs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 143\u001b[0m path_or_handle \u001b[38;5;241m=\u001b[39m handles\u001b[38;5;241m.\u001b[39mhandle\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\common.py:872\u001b[0m, in \u001b[0;36mget_handle\u001b[1;34m(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)\u001b[0m\n\u001b[0;32m 863\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mopen\u001b[39m(\n\u001b[0;32m 864\u001b[0m handle,\n\u001b[0;32m 865\u001b[0m ioargs\u001b[38;5;241m.\u001b[39mmode,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 868\u001b[0m newline\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 869\u001b[0m )\n\u001b[0;32m 870\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 871\u001b[0m \u001b[38;5;66;03m# Binary mode\u001b[39;00m\n\u001b[1;32m--> 872\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mhandle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mioargs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 873\u001b[0m handles\u001b[38;5;241m.\u001b[39mappend(handle)\n\u001b[0;32m 875\u001b[0m \u001b[38;5;66;03m# Convert BytesIO or file objects passed with an encoding\u001b[39;00m\n", - "\u001b[1;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/ovm.parquet'" - ] - } - ], + "outputs": [], "source": [ + "test_data_dir = Path(aeq_dir) / \"tests\" / \"data\" / \"overture\" / 'theme=transportation' \n", "\n", - "os.getcwd()\n", - "df = pd.read_parquet('../data/ovm/type=transportation/type=segment/ovm.parquet')\n", - "df\n", - "# gdf = gpd.GeoDOataFrame(data=df, geometry=df.geometry.apply(shapely.wkb.loads))" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[8], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m df \u001b[38;5;241m=\u001b[39m \u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 2\u001b[0m gdf \u001b[38;5;241m=\u001b[39m gpd\u001b[38;5;241m.\u001b[39mGeoDataFrame(data \u001b[38;5;241m=\u001b[39m df, geometry\u001b[38;5;241m=\u001b[39mdf\u001b[38;5;241m.\u001b[39mgeometry\u001b[38;5;241m.\u001b[39mapply(shapely\u001b[38;5;241m.\u001b[39mwkb\u001b[38;5;241m.\u001b[39mloads))\n\u001b[0;32m 4\u001b[0m gdf\u001b[38;5;241m.\u001b[39mplot()\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:670\u001b[0m, in \u001b[0;36mread_parquet\u001b[1;34m(path, engine, columns, storage_options, use_nullable_dtypes, dtype_backend, filesystem, filters, **kwargs)\u001b[0m\n\u001b[0;32m 667\u001b[0m use_nullable_dtypes \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[0;32m 668\u001b[0m check_dtype_backend(dtype_backend)\n\u001b[1;32m--> 670\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mimpl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 671\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 672\u001b[0m \u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 673\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 674\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 675\u001b[0m \u001b[43m \u001b[49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43muse_nullable_dtypes\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 676\u001b[0m \u001b[43m \u001b[49m\u001b[43mdtype_backend\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdtype_backend\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 677\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 678\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 679\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:265\u001b[0m, in \u001b[0;36mPyArrowImpl.read\u001b[1;34m(self, path, columns, filters, use_nullable_dtypes, dtype_backend, storage_options, filesystem, **kwargs)\u001b[0m\n\u001b[0;32m 262\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m manager \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124marray\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 263\u001b[0m to_pandas_kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msplit_blocks\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m \u001b[38;5;66;03m# type: ignore[assignment]\u001b[39;00m\n\u001b[1;32m--> 265\u001b[0m path_or_handle, handles, filesystem \u001b[38;5;241m=\u001b[39m \u001b[43m_get_path_or_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 266\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 267\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 268\u001b[0m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 269\u001b[0m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 270\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 271\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 272\u001b[0m pa_table \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapi\u001b[38;5;241m.\u001b[39mparquet\u001b[38;5;241m.\u001b[39mread_table(\n\u001b[0;32m 273\u001b[0m path_or_handle,\n\u001b[0;32m 274\u001b[0m columns\u001b[38;5;241m=\u001b[39mcolumns,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 277\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[0;32m 278\u001b[0m )\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\parquet.py:139\u001b[0m, in \u001b[0;36m_get_path_or_handle\u001b[1;34m(path, fs, storage_options, mode, is_dir)\u001b[0m\n\u001b[0;32m 129\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[0;32m 131\u001b[0m \u001b[38;5;129;01mnot\u001b[39;00m fs\n\u001b[0;32m 132\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_dir\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 137\u001b[0m \u001b[38;5;66;03m# fsspec resources can also point to directories\u001b[39;00m\n\u001b[0;32m 138\u001b[0m \u001b[38;5;66;03m# this branch is used for example when reading from non-fsspec URLs\u001b[39;00m\n\u001b[1;32m--> 139\u001b[0m handles \u001b[38;5;241m=\u001b[39m \u001b[43mget_handle\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 140\u001b[0m \u001b[43m \u001b[49m\u001b[43mpath_or_handle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mis_text\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstorage_options\u001b[49m\n\u001b[0;32m 141\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 142\u001b[0m fs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 143\u001b[0m path_or_handle \u001b[38;5;241m=\u001b[39m handles\u001b[38;5;241m.\u001b[39mhandle\n", - "File \u001b[1;32mc:\\Users\\penny\\git\\Aequilibrae\\.venv\\Lib\\site-packages\\pandas\\io\\common.py:872\u001b[0m, in \u001b[0;36mget_handle\u001b[1;34m(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)\u001b[0m\n\u001b[0;32m 863\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mopen\u001b[39m(\n\u001b[0;32m 864\u001b[0m handle,\n\u001b[0;32m 865\u001b[0m ioargs\u001b[38;5;241m.\u001b[39mmode,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 868\u001b[0m newline\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 869\u001b[0m )\n\u001b[0;32m 870\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 871\u001b[0m \u001b[38;5;66;03m# Binary mode\u001b[39;00m\n\u001b[1;32m--> 872\u001b[0m handle \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mhandle\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mioargs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmode\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 873\u001b[0m handles\u001b[38;5;241m.\u001b[39mappend(handle)\n\u001b[0;32m 875\u001b[0m \u001b[38;5;66;03m# Convert BytesIO or file objects passed with an encoding\u001b[39;00m\n", - "\u001b[1;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet'" - ] - } - ], - "source": [ - "df = pd.read_parquet('../data/ovm/type=transportation/type=segment/part-00232-8d133ca6-6cbd-48b8-87d5-a0850a2ba489.c003.zstd.parquet')\n", - "gdf = gpd.GeoDataFrame(data = df, geometry=df.geometry.apply(shapely.wkb.loads))\n", "\n", - "gdf.plot()\n" + "# ovm_downloader_instance = OVMDownloader([\"car\"], test_data_dir)\n", + "# ovm_downloader_instance._download_test_data('E:/theme=transportation/type=segment')\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -149,7 +57,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/tests/data/ovm/type=transportation/type=segment/ovm.parquet b/tests/data/ovm/type=transportation/type=segment/ovm.parquet deleted file mode 100755 index 13511d3c262194bed2bfb8f603fbcd98392dbe84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51967 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 z 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 Date: Thu, 1 Feb 2024 16:36:41 +1000 Subject: [PATCH 4/8] clean up --- aequilibrae/project/network/ovm_downloader.py | 2 +- .../project/ovm/test_ovm_downloader.py | 77 ++++++------------- 2 files changed, 25 insertions(+), 54 deletions(-) diff --git a/aequilibrae/project/network/ovm_downloader.py b/aequilibrae/project/network/ovm_downloader.py index 1509ec57c..4e9038073 100644 --- a/aequilibrae/project/network/ovm_downloader.py +++ b/aequilibrae/project/network/ovm_downloader.py @@ -105,7 +105,7 @@ def downloadPlace(self, source, local_file_path=None): def downloadTransportation(self, bbox: list, data_source: Union[str, Path], output_dir: Union[str, Path]): data_source = Path(data_source) or DEFAULT_OVM_S3_LOCATION - output_dir = Path(output_dir) + output_dir = Path(output_dir) / "theme=transportation" output_file_link = output_dir / f'type=segment' / f'transportation_data_segment.parquet' output_file_node = output_dir / f'type=connector' / f'transportation_data_connector.parquet' diff --git a/tests/aequilibrae/project/ovm/test_ovm_downloader.py b/tests/aequilibrae/project/ovm/test_ovm_downloader.py index 5a870cb29..b67cc3705 100644 --- a/tests/aequilibrae/project/ovm/test_ovm_downloader.py +++ b/tests/aequilibrae/project/ovm/test_ovm_downloader.py @@ -1,61 +1,32 @@ -import importlib.util as iutil import tempfile from pathlib import Path -from tempfile import gettempdir, mkdtemp -from unittest import TestCase -import os -import geopandas as gpd from aequilibrae.project.network.ovm_downloader import OVMDownloader -from random import random -spec = iutil.find_spec("PyQt5") -pyqt = spec is not None +data_dir = Path(__file__).parent.parent.parent.parent / "data" / "overture" / "theme=transportation" -test_data_dir = Path(__file__) -data_dir = Path(__file__).parent.parent.parent.parent / "data" / "overture" / "theme=transportation" +def test_download(): + with tempfile.TemporaryDirectory() as output_dir: + o = OVMDownloader(["car"], output_dir) + + box1 = [148.713909, -20.272261, 148.7206475, -20.2702697] + gdf_link, gdf_node = o.downloadTransportation(bbox=box1, data_source=data_dir, output_dir=output_dir) + + for t in ["segment", "connector"]: + output_dir = Path(output_dir) + # bbo = [148.71641, -20.27082, 148.71861, -20.27001] + # woolworths_parkinglot = [148.718, -20.27049, 148.71889, -20.27006] + expected_file = output_dir / f"theme=transportation" / f"type={t}" / f"transportation_data_{t}.parquet" + assert expected_file.exists() + + link_columns = ["ovm_id", "connectors", "direction", "link_type", "name", "speed", "road", "geometry"] + for element in link_columns: + assert element in gdf_link.columns + + node_columns = ["ovm_id", "geometry"] + for element in node_columns: + assert element in gdf_node.columns + # assert 'is_centroid' in gdf_node.columns + # assert ['unknown', 'secondary', 'residential', 'parkingAisle'] == list(list_gdf[0]['link_type'].unique()) -class TestOVMDownloader(TestCase): - def setUp(self) -> None: - os.environ["PATH"] = os.path.join(gettempdir(), "temp_data") + ";" + os.environ["PATH"] - self.pth = Path(mkdtemp(prefix="aequilibrae")) - - def test_download(self): - # if not self.should_do_work(): - # return - o = OVMDownloader(["car"], self.pth) - with tempfile.TemporaryDirectory() as output_dir: - list_element = 0 - for t in ['segment', 'connector']: - output_dir = Path(output_dir) - bbo =[148.71641, -20.27082, 148.71861, -20.27001] - box1 = [148.713909, -20.272261, 148.7206475,-20.2702697] - woolworths_parkinglot = [148.718, -20.27049, 148.71889, -20.27006] - o.downloadTransportation(bbox=box1, data_source=data_dir, output_dir=output_dir) - list_gdf = o.g_dataframes - expected_file = output_dir / f'theme=transportation' / f"type={t}" / f"transportation_data_{t}.parquet" - assert expected_file.exists() - - gdf = list_gdf[list_element] - gdf_link = list_gdf[0] - gdf_node = list_gdf[1] - - assert gdf.shape[0] > 0 - - link_columns = ['ovm_id', 'connectors', 'direction', 'link_type', 'name', 'speed', 'road', 'geometry'] - for element in link_columns: - assert element in gdf_link.columns - - node_columns = ['ovm_id', 'geometry'] - for element in node_columns: - assert element in gdf_node.columns - - # assert 'is_centroid' in gdf_node.columns - # assert ['unknown', 'secondary', 'residential', 'parkingAisle'] == list(list_gdf[0]['link_type'].unique()) - - list_element+=1 - - def should_do_work(self): - thresh = 1.01 if os.environ.get("GITHUB_WORKFLOW", "ERROR") == "Code coverage" else 0.02 - return random() < thresh From 1584fb74ee78613a2b862052eadeaca6a5783c13 Mon Sep 17 00:00:00 2001 From: Jamie Cook Date: Thu, 1 Feb 2024 16:53:29 +1000 Subject: [PATCH 5/8] tests --- .../project/ovm/test_ovm_processor.py | 372 +++++++++--------- 1 file changed, 189 insertions(+), 183 deletions(-) diff --git a/tests/aequilibrae/project/ovm/test_ovm_processor.py b/tests/aequilibrae/project/ovm/test_ovm_processor.py index d4b5c34c2..b157d0d19 100644 --- a/tests/aequilibrae/project/ovm/test_ovm_processor.py +++ b/tests/aequilibrae/project/ovm/test_ovm_processor.py @@ -1,226 +1,232 @@ import copy -import json import tempfile -import pandas as pd from pathlib import Path -from tempfile import gettempdir, mkdtemp import geopandas as gpd import shapely -from aequilibrae.project.network.ovm_downloader import OVMDownloader from unittest import TestCase -import os -from uuid import uuid4 -from os.path import join from aequilibrae import Project from aequilibrae.project.network.ovm_builder import OVMBuilder -class TestOVMProcessor(TestCase): - def setUp(self) -> None: - os.environ["PATH"] = os.path.join(gettempdir(), "temp_data") + ";" + os.environ["PATH"] - self.pth = Path(mkdtemp(prefix="aequilibrae")) - self.fldr = join(gettempdir(), uuid4().hex) - self.project = Project() - self.project.new(self.fldr) +def test_link_geo_trimmer(): + node1 = (148.7165148, -20.273062) + node2 = (148.7164104, -20.2730078) + geo = shapely.LineString([(148.7165748, -20.2730668), node1, (148.7164585, -20.2730418), node2]) + link_gdf = gpd.GeoDataFrame([[1, 2, geo]], columns=["a_node", "b_node", "geometry"]) + new_geom = copy.copy(link_gdf) - def test_link_geo_trimmer(self): - node1 = (148.7165148, -20.273062) - node2 = (148.7164104, -20.2730078) - geo = shapely.LineString([(148.7165748, -20.2730668), node1, (148.7164585, -20.2730418), node2]) - link_gdf = gpd.GeoDataFrame([[1, 2, geo]], columns=["a_node", "b_node", "geometry"]) - new_geom = copy.copy(link_gdf) + node_lu = { + 1: {"lat": node1[1], "long": node1[0], "coord": node1}, + 2: {"lat": node2[1], "long": node2[0], "coord": node2}, + } - node_lu = {1: {'lat': node1[1], 'long': node1[0], 'coord': node1}, - 2: {'lat': node2[1], 'long': node2[0], 'coord': node2}} - - dataframes = [link_gdf, gpd.GeoDataFrame()] - o = OVMBuilder(ovm_download=dataframes, project_path=self.pth, project=self.project) + with tempfile.TemporaryDirectory() as output_dir: + output_dir = Path(output_dir) + project = Project() + project.new(output_dir / "project") + o = OVMBuilder(link_gdf, gpd.GeoDataFrame(), project_path=output_dir / "project", project=project) # Iterate over the correct range - new_geom['geometry'] = [o.trim_geometry(node_lu, row) for e, row in link_gdf.iterrows()] - + new_geom["geometry"] = [o.trim_geometry(node_lu, row) for e, row in link_gdf.iterrows()] # Assuming you want to assert the length of the new geometry - assert len(new_geom['geometry'][0].coords) == 3 + assert len(new_geom["geometry"][0].coords) == 3 # Assuming you want to assert the correctness of the new geometry # If you don't need the difference operation, you can skip it - + for i in range(0, len(link_gdf)): - if i > 0: + if i > 0: assert new_geom["geometry"][i] == shapely.LineString([node1, (148.7164585, -20.2730418), node2]) - - def test_link_lanes(self): - """ - segment and node infomation is currently [1] element of links when running from_ovm.py - """ - - no_info = None - simple = [{"direction": "backward"}, - {"direction": "forward"}] - - lanes_3 = [{'direction': 'forward', 'restrictions': {'access': [{'allowed': {'when': {'mode': ['hov']}}}], - 'minOccupancy': {'isAtLeast': 3}}}, - {'direction': 'forward'}, - {'direction': 'forward'}] - - highway = [{"direction": "backward"}, {"direction": "backward"}, - {"direction": "backward"}, {"direction": "backward"}, - {"direction": "forward"}, {"direction": "forward"}, - {"direction": "forward"}, {"direction": "forward"}] - - lane_ends = [[{'at': [0, 0.67], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}, - {'direction': 'forward'}]}], - [{'at': [0.67, 1], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}]}]] - - lane_begins = [[{'at': [0, 0.2], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}]}], - [{'at': [0.2, 1], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}, - {'direction': 'forward'}]}]] - - lane_merge_twice = [[{'at': [0, 0.2], - 'value':[ - {'direction': 'backward'}, - {'direction': 'backward'}, - {'direction': 'forward'}, - {'direction': 'forward'}]}], - [{'at': [0.2, 0.8], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}, - {'direction': 'forward'}]}], - [{'at': [0.8, 1], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}]}]] - - equal_dis = [[{'at': [0, 0.5], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}, - {'direction': 'forward'}]}], - [{'at': [0.5, 1], - 'value':[ - {'direction': 'backward'}, - {'direction': 'forward'}]}]] - - def road(lane): - road_info = str({"class":"secondary", - "surface":"paved", - "restrictions":{"speedLimits":{"maxSpeed":[70,"km/h"]}}, - "roadNames":{"common":[{"language":"local","value":"Shute Harbour Road"}]}, - "lanes": lane}) - return road_info - - a_node = {'ovm_id': '8f9d0e128cd9709-167FF64A37F1BFFB', 'geometry': shapely.Point(148.72460, -20.27472)} - b_node = {'ovm_id': '8f9d0e128cd98d6-15FFF68E65613FDF', 'geometry': shapely.Point(148.72471, -20.27492)} - node_df = gpd.GeoDataFrame(data=[a_node,b_node]) - - def segment(direction, road): - segment = {'ovm_id': '8b9d0e128cd9fff-163FF6797FC40661', 'connectors': ['8f9d0e128cd9709-167FF64A37F1BFFB', '8f9d0e128cd98d6-15FFF68E65613FDF'], 'direction': direction, - 'link_type': 'secondary', 'name': 'Shute Harbour Road', 'speed': '{"maxSpeed":[70,"km/h"]}', 'road': road, - 'geometry': shapely.LineString([(148.7245987, -20.2747175), (148.7246504, -20.2747531), (148.724688, -20.274802), (148.7247077, -20.2748593), (148.7247078, -20.2749195)])} - return segment - - dataframes = [gpd.GeoDataFrame(), node_df] - o = OVMBuilder(ovm_download=dataframes, project_path=self.pth, project=self.project) - - with tempfile.TemporaryDirectory() as output_dir: - - # for lane_type in [no_info, simple, lanes_3, highway, lane_ends]: - # print(gpd.GeoDataFrame(segment(lane_type, road(lane_type)))['connectors']) - # print(node_df) - - # assert type(road(lane_type)) == str - # assert type(o.split_connectors(segment(lane_type, road(lane_type)))) == gpd.GeoDataFrame - - dataframes = [gpd.GeoDataFrame(segment(no_info, road(no_info))), node_df] - o = OVMBuilder(ovm_download=dataframes, project_path=self.pth, project=self.project) - # o.__worksetup() - o.create_node_ids(node_df) - # gdf_no_info = o.split_connectors(segment(no_info, road(no_info))) - gdf_no_info = o.formatting(dataframes[0], dataframes[1], output_dir) - print(gdf_no_info.columns) - - assert gdf_no_info['direction'][0] == 0 - assert gdf_no_info['lanes_ab'][0] == 1 - assert gdf_no_info['lanes_ba'][0] == 1 - print('gdf_no_info test: passed') - + +def test_link_lanes(): + """ + segment and node infomation is currently [1] element of links when running from_ovm.py + """ + + no_info = None + simple = [{"direction": "backward"}, {"direction": "forward"}] + + lanes_3 = [ + { + "direction": "forward", + "restrictions": { + "access": [{"allowed": {"when": {"mode": ["hov"]}}}], + "minOccupancy": {"isAtLeast": 3}, + }, + }, + {"direction": "forward"}, + {"direction": "forward"}, + ] + + highway = [ + {"direction": "backward"}, + {"direction": "backward"}, + {"direction": "backward"}, + {"direction": "backward"}, + {"direction": "forward"}, + {"direction": "forward"}, + {"direction": "forward"}, + {"direction": "forward"}, + ] + + lane_ends = [ + [ + { + "at": [0, 0.67], + "value": [{"direction": "backward"}, {"direction": "forward"}, {"direction": "forward"}], + } + ], + [{"at": [0.67, 1], "value": [{"direction": "backward"}, {"direction": "forward"}]}], + ] + + lane_begins = [ + [{"at": [0, 0.2], "value": [{"direction": "backward"}, {"direction": "forward"}]}], + [ + { + "at": [0.2, 1], + "value": [{"direction": "backward"}, {"direction": "forward"}, {"direction": "forward"}], + } + ], + ] + + lane_merge_twice = [ + [ + { + "at": [0, 0.2], + "value": [ + {"direction": "backward"}, + {"direction": "backward"}, + {"direction": "forward"}, + {"direction": "forward"}, + ], + } + ], + [ + { + "at": [0.2, 0.8], + "value": [{"direction": "backward"}, {"direction": "forward"}, {"direction": "forward"}], + } + ], + [{"at": [0.8, 1], "value": [{"direction": "backward"}, {"direction": "forward"}]}], + ] + + equal_dis = [ + [ + { + "at": [0, 0.5], + "value": [{"direction": "backward"}, {"direction": "forward"}, {"direction": "forward"}], + } + ], + [{"at": [0.5, 1], "value": [{"direction": "backward"}, {"direction": "forward"}]}], + ] + + def road(lane): + road_info = str( + { + "class": "secondary", + "surface": "paved", + "restrictions": {"speedLimits": {"maxSpeed": [70, "km/h"]}}, + "roadNames": {"common": [{"language": "local", "value": "Shute Harbour Road"}]}, + "lanes": lane, + } + ) + return road_info + + a_node = {"ovm_id": "8f9d0e128cd9709-167FF64A37F1BFFB", "geometry": shapely.Point(148.72460, -20.27472)} + b_node = {"ovm_id": "8f9d0e128cd98d6-15FFF68E65613FDF", "geometry": shapely.Point(148.72471, -20.27492)} + node_df = gpd.GeoDataFrame(data=[a_node, b_node]) + + def segment(direction, road): + segment = { + "ovm_id": "8b9d0e128cd9fff-163FF6797FC40661", + "connectors": ["8f9d0e128cd9709-167FF64A37F1BFFB", "8f9d0e128cd98d6-15FFF68E65613FDF"], + "direction": direction, + "link_type": "secondary", + "name": '[{"value": "Shute Harbour Road"}]', + "speed": '{"maxSpeed":[70,"km/h"]}', + "road": road, + "geometry": shapely.LineString( + [ + (148.7245987, -20.2747175), + (148.7246504, -20.2747531), + (148.724688, -20.274802), + (148.7247077, -20.2748593), + (148.7247078, -20.2749195), + ] + ), + } + return segment + + with tempfile.TemporaryDirectory() as output_dir: + output_dir = Path(output_dir) + project = Project() + project.new(output_dir / "project") + + link_gdf = gpd.GeoDataFrame(segment(no_info, road(no_info))) + o = OVMBuilder(link_gdf, node_df, project_path=output_dir / "project", project=project) + o.create_node_ids(node_df) + gdf_no_info = o.formatting(link_gdf, node_df, output_dir) + + assert gdf_no_info["direction"][0] == 0 + assert gdf_no_info["lanes_ab"][0] == 1 + assert gdf_no_info["lanes_ba"][0] == 1 + gdf_simple = o.split_connectors(segment(simple, road(simple))) assert len(simple) == 2 - assert gdf_simple['direction'][0] == 0 - assert gdf_simple['lanes_ab'][0] == 1 - assert gdf_simple['lanes_ab'][0] == 1 - print('gdf_simple test: passed') - + assert gdf_simple["direction"][0] == 0 + assert gdf_simple["lanes_ab"][0] == 1 + assert gdf_simple["lanes_ab"][0] == 1 + gdf_lanes_3 = o.split_connectors(segment(lanes_3, road(lanes_3))) - + assert len(lanes_3) == 3 - assert gdf_lanes_3['direction'][0] == 1 - assert gdf_lanes_3['lanes_ab'][0] == 3 - assert gdf_lanes_3['lanes_ba'][0] == None - print('gdf_lanes_3 test: passed') - + assert gdf_lanes_3["direction"][0] == 1 + assert gdf_lanes_3["lanes_ab"][0] == 3 + assert gdf_lanes_3["lanes_ba"][0] == None + gdf_highway = o.split_connectors(segment(highway, road(highway))) - + assert len(highway) == 8 - assert gdf_highway['direction'][0] == 0 - assert gdf_highway['lanes_ab'][0] == 4 - assert gdf_highway['lanes_ba'][0] == 4 - print('gdf_highway test: passed') - + assert gdf_highway["direction"][0] == 0 + assert gdf_highway["lanes_ab"][0] == 4 + assert gdf_highway["lanes_ba"][0] == 4 + gdf_lane_ends = o.split_connectors(segment(lane_ends, road(lane_ends))) assert len(lane_ends) == 2 - assert len(lane_ends[0][0]['value']) == 3 - assert len(lane_ends[1][0]['value']) == 2 - assert gdf_lane_ends['direction'][0] == 0 - assert gdf_lane_ends['lanes_ab'][0] == 2 - assert gdf_lane_ends['lanes_ba'][0] == 1 - print('gdf_lane_ends test: passed') + assert len(lane_ends[0][0]["value"]) == 3 + assert len(lane_ends[1][0]["value"]) == 2 + assert gdf_lane_ends["direction"][0] == 0 + assert gdf_lane_ends["lanes_ab"][0] == 2 + assert gdf_lane_ends["lanes_ba"][0] == 1 gdf_lane_begins = o.split_connectors(segment(lane_begins, road(lane_begins))) assert len(lane_begins) == 2 - assert len(lane_begins[0][0]['value']) == 2 - assert len(lane_begins[1][0]['value']) == 3 - assert gdf_lane_begins['direction'][0] == 0 - assert gdf_lane_begins['lanes_ab'][0] == 2 - assert gdf_lane_begins['lanes_ba'][0] == 1 - print('gdf_lane_begins test: passed') - - + assert len(lane_begins[0][0]["value"]) == 2 + assert len(lane_begins[1][0]["value"]) == 3 + assert gdf_lane_begins["direction"][0] == 0 + assert gdf_lane_begins["lanes_ab"][0] == 2 + assert gdf_lane_begins["lanes_ba"][0] == 1 + gdf_lane_merge_twice = o.split_connectors(segment(lane_merge_twice, road(lane_merge_twice))) assert len(lane_merge_twice) == 3 - assert len(lane_merge_twice[0][0]['value']) == 4 - assert len(lane_merge_twice[1][0]['value']) == 3 - assert len(lane_merge_twice[2][0]['value']) == 2 - assert gdf_lane_merge_twice['direction'][0] == 0 - assert gdf_lane_merge_twice['lanes_ab'][0] == 2 - assert gdf_lane_merge_twice['lanes_ba'][0] == 1 - print('gdf_lane_merge_twice test: passed') + assert len(lane_merge_twice[0][0]["value"]) == 4 + assert len(lane_merge_twice[1][0]["value"]) == 3 + assert len(lane_merge_twice[2][0]["value"]) == 2 + assert gdf_lane_merge_twice["direction"][0] == 0 + assert gdf_lane_merge_twice["lanes_ab"][0] == 2 + assert gdf_lane_merge_twice["lanes_ba"][0] == 1 gdf_equal_dis = o.split_connectors(segment(equal_dis, road(equal_dis))) assert len(equal_dis) == 2 - assert len(equal_dis[0][0]['value']) == 3 - assert len(equal_dis[1][0]['value']) == 2 - assert gdf_equal_dis['direction'][0] == 0 - assert gdf_equal_dis['lanes_ab'][0] == 2 - assert gdf_equal_dis['lanes_ba'][0] == 1 - print('gdf_lane_ends test: passed') \ No newline at end of file + assert len(equal_dis[0][0]["value"]) == 3 + assert len(equal_dis[1][0]["value"]) == 2 + assert gdf_equal_dis["direction"][0] == 0 + assert gdf_equal_dis["lanes_ab"][0] == 2 + assert gdf_equal_dis["lanes_ba"][0] == 1 From e2423b89b53cce8c6e416bdadaeabea46c356cf1 Mon Sep 17 00:00:00 2001 From: Jamie Cook Date: Thu, 1 Feb 2024 16:55:53 +1000 Subject: [PATCH 6/8] . --- .../aequilibrae/paths/Reg_Spiess_3_Vols.ipynb | 433 ------------------ 1 file changed, 433 deletions(-) delete mode 100644 tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb diff --git a/tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb b/tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb deleted file mode 100644 index f75abba08..000000000 --- a/tests/aequilibrae/paths/Reg_Spiess_3_Vols.ipynb +++ /dev/null @@ -1,433 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import uuid\n", - "import zipfile\n", - "from os.path import join, dirname\n", - "from tempfile import gettempdir\n", - "from unittest import TestCase\n", - "import pandas as pd\n", - "import numpy as np\n", - "\n", - "from aequilibrae import TrafficAssignment, TrafficClass, Graph, Project\n", - "from tests.data import siouxfalls_project\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", - " build_compressed_graph(self)\n", - "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", - " build_compressed_graph(self)\n", - "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", - " build_compressed_graph(self)\n", - "C:\\Users\\penny\\git\\Aequilibrae\\aequilibrae\\paths\\graph.py:146: FutureWarning: The behavior of array concatenation with empty entries is deprecated. In a future version, this will no longer exclude empty items when determining the result dtype. To retain the old behavior, exclude the empty entries before the concat operation.\n", - " build_compressed_graph(self)\n" - ] - } - ], - "source": [ - "proj_path ='coming'\n", - "# Initialise project:\n", - "project = Project()\n", - "project.open(proj_path)\n", - "project.network.build_graphs()\n", - "car_graph = project.network.graphs[\"c\"] # type: Graph\n", - "\n", - "car_graph.set_graph(\"free_flow_time\")\n", - "car_graph.set_blocked_centroid_flows(False)\n", - "matrix = project.matrices.get_matrix(\"demand_omx\")\n", - "matrix.computational_view()\n", - "\n", - "# Extra data specific to ODME:\n", - "index = car_graph.nodes_to_indices\n", - "dims = matrix.matrix_view.shape\n", - "count_vol_cols = [\"class\", \"link_id\", \"direction\", \"obs_volume\"]\n", - "\n", - "# Initial assignment parameters:\n", - "assignment = TrafficAssignment()\n", - "assignclass = TrafficClass(\"car\", car_graph, matrix)\n", - "assignment.set_classes([assignclass])\n", - "assignment.set_vdf(\"BPR\")\n", - "assignment.set_vdf_parameters({\"alpha\": 0.15, \"beta\": 4.0})\n", - "assignment.set_vdf_parameters({\"alpha\": \"b\", \"beta\": \"power\"})\n", - "assignment.set_capacity_field(\"capacity\")\n", - "assignment.set_time_field(\"free_flow_time\")\n", - "assignment.max_iter = 5\n", - "assignment.set_algorithm(\"msa\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# Get original flows:.\n", - "assignment.execute()\n", - "assign_df = assignment.results().reset_index(drop=False).fillna(0)\n", - "# SQUISH EXTRA DIMENSION FOR NOW - DEAL WITH THIS PROPERLY LATER ON!!!\n", - "matrix.matrix_view = np.squeeze(matrix.matrix_view, axis=2)\n", - "\n", - "# Set the observed count volumes:\n", - "flow = lambda i: assign_df.loc[assign_df[\"link_id\"] == i, \"matrix_ab\"].values[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
link_idmatrix_abmatrix_bamatrix_totCongested_Time_ABCongested_Time_BACongested_Time_MaxDelay_factor_ABDelay_factor_BADelay_factor_MaxVOC_ABVOC_BAVOC_maxPCE_ABPCE_BAPCE_tot
015880.00.05880.06.0023910.06.0023911.0003980.01.0003980.2270250.00.2270255880.00.05880.0
1210540.00.010540.04.0246830.04.0246831.0061710.01.0061710.4503610.00.45036110540.00.010540.0
236540.00.06540.06.0036590.06.0036591.0006100.01.0006100.2525080.00.2525086540.00.06540.0
346000.00.06000.06.6083360.06.6083361.3216670.01.3216671.2101210.01.2101216000.00.06000.0
459880.00.09880.04.0190570.04.0190571.0047640.01.0047640.4221600.00.4221609880.00.09880.0
...................................................
717211460.00.011460.020.5580700.020.5580705.1395170.05.1395172.2920000.02.29200011460.00.011460.0
727311320.00.011320.09.4056340.09.4056344.7028170.04.7028172.2290010.02.22900111320.00.011320.0
737411680.00.011680.020.6196870.020.6196875.1549220.05.1549222.2941290.02.29412911680.00.011680.0
74758860.00.08860.07.8681280.07.8681282.6227090.02.6227091.8135830.01.8135838860.00.08860.0
757612260.00.012260.012.1891610.012.1891616.0945800.06.0945802.4140950.02.41409512260.00.012260.0
\n", - "

76 rows × 16 columns

\n", - "
" - ], - "text/plain": [ - " link_id matrix_ab matrix_ba matrix_tot Congested_Time_AB \\\n", - "0 1 5880.0 0.0 5880.0 6.002391 \n", - "1 2 10540.0 0.0 10540.0 4.024683 \n", - "2 3 6540.0 0.0 6540.0 6.003659 \n", - "3 4 6000.0 0.0 6000.0 6.608336 \n", - "4 5 9880.0 0.0 9880.0 4.019057 \n", - ".. ... ... ... ... ... \n", - "71 72 11460.0 0.0 11460.0 20.558070 \n", - "72 73 11320.0 0.0 11320.0 9.405634 \n", - "73 74 11680.0 0.0 11680.0 20.619687 \n", - "74 75 8860.0 0.0 8860.0 7.868128 \n", - "75 76 12260.0 0.0 12260.0 12.189161 \n", - "\n", - " Congested_Time_BA Congested_Time_Max Delay_factor_AB Delay_factor_BA \\\n", - "0 0.0 6.002391 1.000398 0.0 \n", - "1 0.0 4.024683 1.006171 0.0 \n", - "2 0.0 6.003659 1.000610 0.0 \n", - "3 0.0 6.608336 1.321667 0.0 \n", - "4 0.0 4.019057 1.004764 0.0 \n", - ".. ... ... ... ... \n", - "71 0.0 20.558070 5.139517 0.0 \n", - "72 0.0 9.405634 4.702817 0.0 \n", - "73 0.0 20.619687 5.154922 0.0 \n", - "74 0.0 7.868128 2.622709 0.0 \n", - "75 0.0 12.189161 6.094580 0.0 \n", - "\n", - " Delay_factor_Max VOC_AB VOC_BA VOC_max PCE_AB PCE_BA PCE_tot \n", - "0 1.000398 0.227025 0.0 0.227025 5880.0 0.0 5880.0 \n", - "1 1.006171 0.450361 0.0 0.450361 10540.0 0.0 10540.0 \n", - "2 1.000610 0.252508 0.0 0.252508 6540.0 0.0 6540.0 \n", - "3 1.321667 1.210121 0.0 1.210121 6000.0 0.0 6000.0 \n", - "4 1.004764 0.422160 0.0 0.422160 9880.0 0.0 9880.0 \n", - ".. ... ... ... ... ... ... ... \n", - "71 5.139517 2.292000 0.0 2.292000 11460.0 0.0 11460.0 \n", - "72 4.702817 2.229001 0.0 2.229001 11320.0 0.0 11320.0 \n", - "73 5.154922 2.294129 0.0 2.294129 11680.0 0.0 11680.0 \n", - "74 2.622709 1.813583 0.0 1.813583 8860.0 0.0 8860.0 \n", - "75 6.094580 2.414095 0.0 2.414095 12260.0 0.0 12260.0 \n", - "\n", - "[76 rows x 16 columns]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "assign_df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "interpreter": { - "hash": "acc84914dd5d49aef4abd59151ad776eeaa26fd8e748105fd8228ce1c06cbf3b" - }, - "kernelspec": { - "display_name": "Python 3.10.12 ('venv': venv)", - "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.11.6" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} From fcbdaf39d94704ca4b42fc793b7fb4df919f9260 Mon Sep 17 00:00:00 2001 From: Jamie Cook Date: Fri, 2 Feb 2024 12:43:40 +1000 Subject: [PATCH 7/8] project close --- aequilibrae/project/project.py | 5 +++++ tests/aequilibrae/project/ovm/test_ovm_processor.py | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/aequilibrae/project/project.py b/aequilibrae/project/project.py index bb464c2e9..168bfa880 100644 --- a/aequilibrae/project/project.py +++ b/aequilibrae/project/project.py @@ -130,6 +130,11 @@ def close(self) -> None: global_logger.info(f"Closed project on {self.project_base_path}") + for h in self.logger.handlers: + self.logger.removeHandler(h) + h.close() + + except (sqlite3.ProgrammingError, AttributeError): global_logger.warning(f"This project at {self.project_base_path} is already closed") diff --git a/tests/aequilibrae/project/ovm/test_ovm_processor.py b/tests/aequilibrae/project/ovm/test_ovm_processor.py index b157d0d19..a8281bf60 100644 --- a/tests/aequilibrae/project/ovm/test_ovm_processor.py +++ b/tests/aequilibrae/project/ovm/test_ovm_processor.py @@ -22,10 +22,10 @@ def test_link_geo_trimmer(): } with tempfile.TemporaryDirectory() as output_dir: - output_dir = Path(output_dir) + project_dir = str(Path(output_dir) / "project") project = Project() - project.new(output_dir / "project") - o = OVMBuilder(link_gdf, gpd.GeoDataFrame(), project_path=output_dir / "project", project=project) + project.new(project_dir) + o = OVMBuilder(link_gdf, gpd.GeoDataFrame(), project_path=project_dir, project=project) # Iterate over the correct range new_geom["geometry"] = [o.trim_geometry(node_lu, row) for e, row in link_gdf.iterrows()] @@ -40,6 +40,8 @@ def test_link_geo_trimmer(): if i > 0: assert new_geom["geometry"][i] == shapely.LineString([node1, (148.7164585, -20.2730418), node2]) + project.close() + def test_link_lanes(): """ segment and node infomation is currently [1] element of links when running from_ovm.py From c01f6fca3a2668d45d1b5f37d92b0dc77c42c272 Mon Sep 17 00:00:00 2001 From: 17Pens Date: Sun, 4 Feb 2024 13:16:40 +1000 Subject: [PATCH 8/8] final commint, from_ovm is working --- aequilibrae/project/network/ovm_builder.py | 4 +- aequilibrae/project/network/ovm_downloader.py | 1 - .../examples/creating_models/from_ovm.py | 6 +- .../project/ovm/test_ovm_processor.py | 72 +++++++++++-------- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/aequilibrae/project/network/ovm_builder.py b/aequilibrae/project/network/ovm_builder.py index 8a153338a..45b1aa31d 100644 --- a/aequilibrae/project/network/ovm_builder.py +++ b/aequilibrae/project/network/ovm_builder.py @@ -74,7 +74,7 @@ def __emit_all(self, *args): def doWork(self, output_dir: Path): self.conn = connect_spatialite(self.pth) self.curr = self.conn.cursor() - self.__worksetup() + self._worksetup() self.formatting(self.links_gdf, self.nodes_gdf, output_dir) self.__emit_all(["finished_threaded_procedure", 0]) @@ -187,7 +187,7 @@ def formatting(self, links_gdf: gpd.GeoDataFrame, nodes_gdf: gpd.GeoDataFrame, o del links_gdf self.curr.close() - def __worksetup(self): + def _worksetup(self): self.__link_types = self.project.network.link_types lts = self.__link_types.all_types() for lt_id, lt in lts.items(): diff --git a/aequilibrae/project/network/ovm_downloader.py b/aequilibrae/project/network/ovm_downloader.py index 4e9038073..3b9400eb0 100644 --- a/aequilibrae/project/network/ovm_downloader.py +++ b/aequilibrae/project/network/ovm_downloader.py @@ -133,7 +133,6 @@ def downloadTransportation(self, bbox: list, data_source: Union[str, Path], outp CAST(road AS JSON) ->>'class' AS link_type, CAST(road AS JSON) ->>'roadNames' ->>'common' AS name, CAST(road AS JSON) ->>'restrictions' ->> 'speedLimits' AS speed, - road, geometry FROM read_parquet('{data_source}/type=segment/*', union_by_name=True) WHERE bbox.minx > '{bbox[0]}' diff --git a/docs/source/examples/creating_models/from_ovm.py b/docs/source/examples/creating_models/from_ovm.py index 97cda898a..d83e4d986 100644 --- a/docs/source/examples/creating_models/from_ovm.py +++ b/docs/source/examples/creating_models/from_ovm.py @@ -45,14 +45,14 @@ # We recommend downloading these cloud-native Parquet files to drive and replacing the data_source file to match dir = str(Path('../../../../').resolve()) data_source = Path(dir) / 'tests' / 'data' / 'overture' / 'theme=transportation' -output_dir = Path(fldr) / "raw_parquet" +output_dir = Path(fldr) / "theme=transportation" # For the sake of this example, we will choose the small town of Airlie Beach. # The "bbox" parameter specifies the bounding box encompassing the desired geographical location. In the given example, this refers to the bounding box that encompasses Airlie Beach. bbox = [148.7077, -20.2780, 148.7324, -20.2621 ] # We can create from a bounding box or a named place. -project.network.create_from_ovm(west=bbox[0], south=bbox[1], east=bbox[2], north=bbox[3], data_source=data_source, output_dir=output_dir) +project.network.create_from_ovm(west=bbox[0], south=bbox[1], east=bbox[2], north=bbox[3], data_source=data_source, output_dir=data_source) # %% # We grab all the links data as a Pandas DataFrame so we can process it easier @@ -87,3 +87,5 @@ # %% project.close() + +# %% diff --git a/tests/aequilibrae/project/ovm/test_ovm_processor.py b/tests/aequilibrae/project/ovm/test_ovm_processor.py index a8281bf60..0a1e906f8 100644 --- a/tests/aequilibrae/project/ovm/test_ovm_processor.py +++ b/tests/aequilibrae/project/ovm/test_ovm_processor.py @@ -3,7 +3,7 @@ from pathlib import Path import geopandas as gpd import shapely -from unittest import TestCase +from aequilibrae import global_logger from aequilibrae import Project from aequilibrae.project.network.ovm_builder import OVMBuilder @@ -74,10 +74,9 @@ def test_link_lanes(): ] lane_ends = [ - [ - { - "at": [0, 0.67], - "value": [{"direction": "backward"}, {"direction": "forward"}, {"direction": "forward"}], + [{ + "at": [0, 0.67], + "value": [{"direction": "backward"}, {"direction": "forward"}, {"direction": "forward"}], } ], [{"at": [0.67, 1], "value": [{"direction": "backward"}, {"direction": "forward"}]}], @@ -124,31 +123,30 @@ def test_link_lanes(): [{"at": [0.5, 1], "value": [{"direction": "backward"}, {"direction": "forward"}]}], ] - def road(lane): - road_info = str( - { - "class": "secondary", - "surface": "paved", - "restrictions": {"speedLimits": {"maxSpeed": [70, "km/h"]}}, - "roadNames": {"common": [{"language": "local", "value": "Shute Harbour Road"}]}, - "lanes": lane, - } - ) - return road_info + # def road(lane): + # road_info = str( + # { + # "class": "secondary", + # "surface": "paved", + # "restrictions": {"speedLimits": {"maxSpeed": [70, "km/h"]}}, + # "roadNames": {"common": [{"language": "local", "value": "Shute Harbour Road"}]}, + # "lanes": lane, + # } + # ) + # return road_info a_node = {"ovm_id": "8f9d0e128cd9709-167FF64A37F1BFFB", "geometry": shapely.Point(148.72460, -20.27472)} b_node = {"ovm_id": "8f9d0e128cd98d6-15FFF68E65613FDF", "geometry": shapely.Point(148.72471, -20.27492)} node_df = gpd.GeoDataFrame(data=[a_node, b_node]) - def segment(direction, road): + def segment(direction): segment = { "ovm_id": "8b9d0e128cd9fff-163FF6797FC40661", - "connectors": ["8f9d0e128cd9709-167FF64A37F1BFFB", "8f9d0e128cd98d6-15FFF68E65613FDF"], + "connectors": [["8f9d0e128cd9709-167FF64A37F1BFFB", "8f9d0e128cd98d6-15FFF68E65613FDF"]], "direction": direction, "link_type": "secondary", "name": '[{"value": "Shute Harbour Road"}]', "speed": '{"maxSpeed":[70,"km/h"]}', - "road": road, "geometry": shapely.LineString( [ (148.7245987, -20.2747175), @@ -160,43 +158,57 @@ def segment(direction, road): ), } return segment + + # def link_gdf(lane_info): + # return gpd.GeoDataFrame(segment(lane_info, road(lane_info))) + + def set_up_ovmbuilder(lane_info, output_dir, project): + print(lane_info) + print() + print(segment(lane_info)) + links = gpd.GeoDataFrame(segment(lane_info)) + print(links) + o = OVMBuilder(links, node_df, project_path=output_dir / "project", project=project) + o.create_node_ids(node_df) + o._worksetup() + link_gdf = o.formatting(links, node_df, output_dir) + print(link_gdf) + return link_gdf + with tempfile.TemporaryDirectory() as output_dir: output_dir = Path(output_dir) project = Project() project.new(output_dir / "project") - link_gdf = gpd.GeoDataFrame(segment(no_info, road(no_info))) - o = OVMBuilder(link_gdf, node_df, project_path=output_dir / "project", project=project) - o.create_node_ids(node_df) - gdf_no_info = o.formatting(link_gdf, node_df, output_dir) + gdf_no_info = set_up_ovmbuilder(no_info, output_dir, project) assert gdf_no_info["direction"][0] == 0 assert gdf_no_info["lanes_ab"][0] == 1 assert gdf_no_info["lanes_ba"][0] == 1 - gdf_simple = o.split_connectors(segment(simple, road(simple))) + gdf_simple = set_up_ovmbuilder(simple, output_dir, project) assert len(simple) == 2 assert gdf_simple["direction"][0] == 0 assert gdf_simple["lanes_ab"][0] == 1 assert gdf_simple["lanes_ab"][0] == 1 - gdf_lanes_3 = o.split_connectors(segment(lanes_3, road(lanes_3))) + gdf_lanes_3 = set_up_ovmbuilder(lanes_3, output_dir, project) assert len(lanes_3) == 3 assert gdf_lanes_3["direction"][0] == 1 assert gdf_lanes_3["lanes_ab"][0] == 3 assert gdf_lanes_3["lanes_ba"][0] == None - gdf_highway = o.split_connectors(segment(highway, road(highway))) + gdf_highway = set_up_ovmbuilder(highway, output_dir, project) assert len(highway) == 8 assert gdf_highway["direction"][0] == 0 assert gdf_highway["lanes_ab"][0] == 4 assert gdf_highway["lanes_ba"][0] == 4 - gdf_lane_ends = o.split_connectors(segment(lane_ends, road(lane_ends))) + gdf_lane_ends = set_up_ovmbuilder(lane_ends, output_dir, project) assert len(lane_ends) == 2 assert len(lane_ends[0][0]["value"]) == 3 @@ -205,7 +217,7 @@ def segment(direction, road): assert gdf_lane_ends["lanes_ab"][0] == 2 assert gdf_lane_ends["lanes_ba"][0] == 1 - gdf_lane_begins = o.split_connectors(segment(lane_begins, road(lane_begins))) + gdf_lane_begins = set_up_ovmbuilder(lane_begins, output_dir, project) assert len(lane_begins) == 2 assert len(lane_begins[0][0]["value"]) == 2 @@ -214,7 +226,7 @@ def segment(direction, road): assert gdf_lane_begins["lanes_ab"][0] == 2 assert gdf_lane_begins["lanes_ba"][0] == 1 - gdf_lane_merge_twice = o.split_connectors(segment(lane_merge_twice, road(lane_merge_twice))) + gdf_lane_merge_twice = set_up_ovmbuilder(lane_merge_twice, output_dir, project) assert len(lane_merge_twice) == 3 assert len(lane_merge_twice[0][0]["value"]) == 4 @@ -224,7 +236,7 @@ def segment(direction, road): assert gdf_lane_merge_twice["lanes_ab"][0] == 2 assert gdf_lane_merge_twice["lanes_ba"][0] == 1 - gdf_equal_dis = o.split_connectors(segment(equal_dis, road(equal_dis))) + gdf_equal_dis = set_up_ovmbuilder(equal_dis, output_dir, project) assert len(equal_dis) == 2 assert len(equal_dis[0][0]["value"]) == 3