From adb9b10d327a034e04d69e40aa55e1eedb977b35 Mon Sep 17 00:00:00 2001 From: Kasia Kozlowska <36536946+KasiaKoz@users.noreply.github.com> Date: Fri, 23 Sep 2022 10:48:52 +0100 Subject: [PATCH] Validation zhuzh-up: link attribute value checks (#141) * refactor link length over 1km * tidy up graph connectivity validation * refactor length and zero value checks * add checks for negative, infinite and fractional link attribute values * fix to work with dictionary / nested attributes * module rename, tidy up * update logging to match the rest * fix imports in tests * address PR comments: Part 1: readability and conditions toolbox dataclass * address PR comments: Part 2: chop up existing tests for validation report * add condition for none values --- example_data/api_requests_send.json | 2 +- genet/core.py | 116 ++++--- genet/output/matsim_xml_writer.py | 2 +- genet/schedule_elements.py | 2 +- genet/utils/dict_support.py | 4 + genet/utils/graph_operations.py | 19 +- genet/validate/network.py | 101 ++++++ genet/validate/network_validation.py | 37 --- .../{schedule_validation.py => schedule.py} | 0 tests/test_core_network.py | 306 +++++++++++++----- tests/test_core_schedule.py | 2 +- tests/test_utils_dict_support.py | 8 + tests/test_utils_graph_operations.py | 18 ++ tests/test_utils_schedule_operations.py | 2 +- tests/test_validate_network.py | 25 ++ 15 files changed, 484 insertions(+), 160 deletions(-) create mode 100644 genet/validate/network.py delete mode 100644 genet/validate/network_validation.py rename genet/validate/{schedule_validation.py => schedule.py} (100%) create mode 100644 tests/test_validate_network.py diff --git a/example_data/api_requests_send.json b/example_data/api_requests_send.json index 59426a51..3c6427d7 100644 --- a/example_data/api_requests_send.json +++ b/example_data/api_requests_send.json @@ -1 +1 @@ -{"('1684410058', '1684410054')": "{'path_nodes': ('1684410058', '1684410054'), 'path_polyline': 'aslyHly[X~C', 'origin': {'id': '1684410058', 'x': 528632.2359216934, 'y': 181249.06604453287, 'lon': -0.1475942870992026, 'lat': 51.515528533240705, 's2_id': 5221390312863352989}, 'destination': {'id': '1684410054', 'x': 528577.228591008, 'y': 181232.8743807681, 'lon': -0.14839248709963276, 'lat': 51.5153955332407, 's2_id': 5221390312432500411}}", "('60035535', '3943984251')": "{'path_nodes': ('60035535', '3943984251'), 'path_polyline': 'uenyHb{V~BqA', 'origin': {'id': '60035535', 'x': 530366.8640776458, 'y': 182194.5456432234, 'lon': -0.12226038708327418, 'lat': 51.523627933239666, 's2_id': 5221390742017931799}, 'destination': {'id': '3943984251', 'x': 530397.3803992561, 'y': 182123.92247810628, 'lon': -0.12184688708347477, 'lat': 51.522986233239784, 's2_id': 5221390741548489905}}", "('1684410054', '1684410058')": "{'path_nodes': ('1684410054', '1684410058'), 'path_polyline': 'grlyHl~[Y_D', 'origin': {'id': '1684410054', 'x': 528577.228591008, 'y': 181232.8743807681, 'lon': -0.14839248709963276, 'lat': 51.5153955332407, 's2_id': 5221390312432500411}, 'destination': {'id': '1684410058', 'x': 528632.2359216934, 'y': 181249.06604453287, 'lon': -0.1475942870992026, 'lat': 51.515528533240705, 's2_id': 5221390312863352989}}", "('305691971', '109349')": "{'path_nodes': ('305691971', '109349'), 'path_polyline': '}fnyHzxVVdA', 'origin': {'id': '305691971', 'x': 530391.2789650426, 'y': 182217.21618592896, 'lon': -0.12190028708299697, 'lat': 51.52382603323963, 's2_id': 5221390742060118677}, 'destination': {'id': '109349', 'x': 530367.4912101941, 'y': 182203.92011985858, 'lon': -0.12224788708321918, 'lat': 51.52371203323968, 's2_id': 5221390742034218987}}", "('14791189', '107848')": "{'path_nodes': ('14791189', '107848'), 'path_polyline': 'agmyHjvXs@iB', 'origin': {'id': '14791189', 'x': 529722.9713333722, 'y': 181633.29913677758, 'lon': -0.13174268709034131, 'lat': 51.5187323332404, 's2_id': 5221390701130112993}, 'destination': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}}", "('1667203491', '1684410054')": "{'path_nodes': ('1667203491', '1684410054'), 'path_polyline': 'gxlyH`a\\\\~DsA', 'origin': {'id': '1667203491', 'x': 528545.5372283176, 'y': 181338.99680078932, 'lon': -0.14881028709924338, 'lat': 51.5163564332406, 's2_id': 5221390311898177595}, 'destination': {'id': '1684410054', 'x': 528577.228591008, 'y': 181232.8743807681, 'lon': -0.14839248709963276, 'lat': 51.5153955332407, 's2_id': 5221390312432500411}}", "('107848', '14791189')": "{'path_nodes': ('107848', '14791189'), 'path_polyline': 'uhmyH`sXr@hB', 'origin': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}, 'destination': {'id': '14791189', 'x': 529722.9713333722, 'y': 181633.29913677758, 'lon': -0.13174268709034131, 'lat': 51.5187323332404, 's2_id': 5221390701130112993}}", "('60035532', '60035533')": "{'path_nodes': ('60035532', '60035533'), 'path_polyline': '}enyH~{VOK', 'origin': {'id': '60035532', 'x': 530356.7410048215, 'y': 182198.95950210653, 'lon': -0.12240458708331098, 'lat': 51.52366993323967, 's2_id': 5221390742020738093}, 'destination': {'id': '60035533', 'x': 530360.7288168034, 'y': 182208.29780931148, 'lon': -0.12234368708323559, 'lat': 51.52375293323967, 's2_id': 5221390742025021247}}", "('60035533', '305691975')": "{'path_nodes': ('60035533', '305691975'), 'path_polyline': 'mfnyHr{Vk@J', 'origin': {'id': '60035533', 'x': 530360.7288168034, 'y': 182208.29780931148, 'lon': -0.12234368708323559, 'lat': 51.52375293323967, 's2_id': 5221390742025021247}, 'destination': {'id': '305691975', 'x': 530356.2034866889, 'y': 182232.3510177004, 'lon': -0.12239998708313073, 'lat': 51.52397013323962, 's2_id': 5221390741976307405}}", "('109349', '60035533')": "{'path_nodes': ('109349', '60035533'), 'path_polyline': 'efnyH`{VGP', 'origin': {'id': '109349', 'x': 530367.4912101941, 'y': 182203.92011985858, 'lon': -0.12224788708321918, 'lat': 51.52371203323968, 's2_id': 5221390742034218987}, 'destination': {'id': '60035533', 'x': 530360.7288168034, 'y': 182208.29780931148, 'lon': -0.12234368708323559, 'lat': 51.52375293323967, 's2_id': 5221390742025021247}}", "('821550', '821559')": "{'path_nodes': ('821550', '821559'), 'path_polyline': 'icnyH|c[jDmA', 'origin': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}, 'destination': {'id': '821559', 'x': 528878.7128509199, 'y': 182019.1831767487, 'lon': -0.1437627870934339, 'lat': 51.52239323323963, 's2_id': 5221390325857124155}}", "('2047806084', '255566')": "{'path_nodes': ('2047806084', '255566'), 'path_polyline': 'krlyHdnZKD', 'origin': {'id': '2047806084', 'x': 529113.1043697781, 'y': 181249.41004932253, 'lon': -0.14066798709622622, 'lat': 51.51542203324082, 's2_id': 5221390688018024529}, 'destination': {'id': '255566', 'x': 529110.8165279367, 'y': 181255.83928485424, 'lon': -0.140698587096205, 'lat': 51.51548033324079, 's2_id': 5221390688019159091}}", "('109349', '60035535')": "{'path_nodes': ('109349', '60035535'), 'path_polyline': 'efnyH`{VN@', 'origin': {'id': '109349', 'x': 530367.4912101941, 'y': 182203.92011985858, 'lon': -0.12224788708321918, 'lat': 51.52371203323968, 's2_id': 5221390742034218987}, 'destination': {'id': '60035535', 'x': 530366.8640776458, 'y': 182194.5456432234, 'lon': -0.12226038708327418, 'lat': 51.523627933239666, 's2_id': 5221390742017931799}}", "('821559', '821550')": "{'path_nodes': ('821559', '821550'), 'path_polyline': '}}myHna[kDlA', 'origin': {'id': '821559', 'x': 528878.7128509199, 'y': 182019.1831767487, 'lon': -0.1437627870934339, 'lat': 51.52239323323963, 's2_id': 5221390325857124155}, 'destination': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}}", "('821550', '25522559')": "{'path_nodes': ('821550', '25522559'), 'path_polyline': 'icnyH|c[uAN', 'origin': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}, 'destination': {'id': '25522559', 'x': 528842.6948471027, 'y': 182161.95959961542, 'lon': -0.14422948709287078, 'lat': 51.523684533239404, 's2_id': 5221390328858015501}}", "('25522558', '821550')": "{'path_nodes': ('25522558', '821550'), 'path_polyline': '}dnyHfb[r@t@', 'origin': {'id': '25522558', 'x': 528867.1745911204, 'y': 182143.19631453254, 'lon': -0.14388368709282368, 'lat': 51.52351033323944, 's2_id': 5221390331520430839}, 'destination': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}}", "('60035532', '60035535')": "{'path_nodes': ('60035532', '60035535'), 'path_polyline': '}enyH~{VF[', 'origin': {'id': '60035532', 'x': 530356.7410048215, 'y': 182198.95950210653, 'lon': -0.12240458708331098, 'lat': 51.52366993323967, 's2_id': 5221390742020738093}, 'destination': {'id': '60035535', 'x': 530366.8640776458, 'y': 182194.5456432234, 'lon': -0.12226038708327418, 'lat': 51.523627933239666, 's2_id': 5221390742017931799}}", "('1616122178', '452486132')": "{'path_nodes': ['1616122178', '1616122145', '452486132'], 'path_polyline': 'cmlyHpiWv@cAbBgD', 'origin': {'id': '1616122178', 'x': 530232.2897525224, 'y': 181184.16290962603, 'lon': -0.12457208708967878, 'lat': 51.51457903324111, 's2_id': 5221366095264680241}, 'destination': {'id': '452486132', 'x': 530316.2083825651, 'y': 181099.19575368112, 'lon': -0.12339478708962881, 'lat': 51.51379613324128, 's2_id': 5221366094895935055}}", "('1684410058', '108252')": "{'path_nodes': ['1684410058', '108239', '25257028', '25256949', '168272', '108252'], 'path_polyline': 'aslyHly[pDgCdBiB\\\\a@tC{CxDaE', 'origin': {'id': '1684410058', 'x': 528632.2359216934, 'y': 181249.06604453287, 'lon': -0.1475942870992026, 'lat': 51.515528533240705, 's2_id': 5221390312863352989}, 'destination': {'id': '108252', 'x': 528858.8179076333, 'y': 180895.9103971371, 'lon': -0.14445968709974794, 'lat': 51.512303233241255, 's2_id': 5221366500350297403}}", "('821559', '311422343')": "{'path_nodes': ['821559', '5560625853', '1694551560', '1685938656', '822403', '102000', '311422343'], 'path_polyline': '}}myHna[x@YVIdC}@dBq@hC{@pEwA', 'origin': {'id': '821559', 'x': 528878.7128509199, 'y': 182019.1831767487, 'lon': -0.1437627870934339, 'lat': 51.52239323323963, 's2_id': 5221390325857124155}, 'destination': {'id': '311422343', 'x': 528990.7943980636, 'y': 181651.66929955478, 'lon': -0.14228258709476566, 'lat': 51.5190649332402, 's2_id': 5221390324044305141}}", "('311422343', '255566')": "{'path_nodes': ['311422343', '102005', '107343', '107345', '691084080', '1684410105', '255562', '255564', '1684410076', '255566'], 'path_polyline': 'cimyHfxZ^OzB{@`A[dA[|Ai@|Ai@jBm@z@YbA[', 'origin': {'id': '311422343', 'x': 528990.7943980636, 'y': 181651.66929955478, 'lon': -0.14228258709476566, 'lat': 51.5190649332402, 's2_id': 5221390324044305141}, 'destination': {'id': '255566', 'x': 529110.8165279367, 'y': 181255.83928485424, 'lon': -0.140698587096205, 'lat': 51.51548033324079, 's2_id': 5221390688019159091}}", "('305691975', '107877')": "{'path_nodes': ['305691975', '109348', '110269', '110270', '107877'], 'path_polyline': 'ygnyH~{VyC~AiCpAeCpAaD|A', 'origin': {'id': '305691975', 'x': 530356.2034866889, 'y': 182232.3510177004, 'lon': -0.12239998708313073, 'lat': 51.52397013323962, 's2_id': 5221390741976307405}, 'destination': {'id': '107877', 'x': 530225.3073599826, 'y': 182556.42574277206, 'lon': -0.12416598708215254, 'lat': 51.5269126332391, 's2_id': 5221390754930610347}}", "('4356572322', '1616122178')": "{'path_nodes': ['4356572322', '1616122237', '1616122178'], 'path_polyline': 'qtlyHtsWzDqFp@qA', 'origin': {'id': '4356572322', 'x': 530116.5515434985, 'y': 181313.1831658283, 'lon': -0.12619148708967906, 'lat': 51.515765133240926, 's2_id': 5221390724501967751}, 'destination': {'id': '1616122178', 'x': 530232.2897525224, 'y': 181184.16290962603, 'lon': -0.12457208708967878, 'lat': 51.51457903324111, 's2_id': 5221366095264680241}}", "('107843', '107842')": "{'path_nodes': ['107843', '4860880474', '107842'], 'path_polyline': 'oomyHdcX`AvCL^', 'origin': {'id': '107843', 'x': 529932.1864720277, 'y': 181788.54521884263, 'lon': -0.12867188708819463, 'lat': 51.520079433240205, 's2_id': 5221390719594778311}, 'destination': {'id': '107842', 'x': 529869.4437073416, 'y': 181742.90904576948, 'lon': -0.1295924870888332, 'lat': 51.51968373324025, 's2_id': 5221390702256103647}}", "('107843', '108014')": "{'path_nodes': ['107843', '983836443', '108014'], 'path_polyline': 'oomyHdcXaAyBmBwF', 'origin': {'id': '107843', 'x': 529932.1864720277, 'y': 181788.54521884263, 'lon': -0.12867188708819463, 'lat': 51.520079433240205, 's2_id': 5221390719594778311}, 'destination': {'id': '108014', 'x': 530058.0320436077, 'y': 181889.4880572388, 'lon': -0.12682188708686193, 'lat': 51.520957633240066, 's2_id': 5221390717852326699}}", "('108208', '110008')": "{'path_nodes': ['108208', '1667118171', '1610964470', '108209', '1612319349', '1612319339', '110008'], 'path_polyline': 'ismyHd|\\\\BtA@hAd@tILrAP`BBV', 'origin': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}, 'destination': {'id': '110008', 'x': 527987.5162014617, 'y': 181762.74365661351, 'lon': -0.1566941871003674, 'lat': 51.520291133239844, 's2_id': 5221390292694281737}}", "('108208', '5560599870')": "{'path_nodes': ['108208', '6342450111', '108210', '1667118184', '5560616885', '25500809', '21651765', '108212', '5560599870'], 'path_polyline': 'ismyHd|\\\\OYkBJAkBCqBFGv@?h@CXE', 'origin': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}, 'destination': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}}", "('107877', '305691975')": "{'path_nodes': ['107877', '110270', '110269', '109348', '305691975'], 'path_polyline': 'eznyH`gW`D}AdCqAhCqAxC_B', 'origin': {'id': '107877', 'x': 530225.3073599826, 'y': 182556.42574277206, 'lon': -0.12416598708215254, 'lat': 51.5269126332391, 's2_id': 5221390754930610347}, 'destination': {'id': '305691975', 'x': 530356.2034866889, 'y': 182232.3510177004, 'lon': -0.12239998708313073, 'lat': 51.52397013323962, 's2_id': 5221390741976307405}}", "('110008', '108208')": "{'path_nodes': ['110008', '1612319339', '1612319349', '108209', '1610964470', '1667118171', '108208'], 'path_polyline': 'ypmyHhr]CWQaBMsAe@uIAiACuA', 'origin': {'id': '110008', 'x': 527987.5162014617, 'y': 181762.74365661351, 'lon': -0.1566941871003674, 'lat': 51.520291133239844, 's2_id': 5221390292694281737}, 'destination': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}}", "('452486132', '1616122178')": "{'path_nodes': ['452486132', '1616122145', '1616122178'], 'path_polyline': 'ghlyHdbWcBfDw@bA', 'origin': {'id': '452486132', 'x': 530316.2083825651, 'y': 181099.19575368112, 'lon': -0.12339478708962881, 'lat': 51.51379613324128, 's2_id': 5221366094895935055}, 'destination': {'id': '1616122178', 'x': 530232.2897525224, 'y': 181184.16290962603, 'lon': -0.12457208708967878, 'lat': 51.51457903324111, 's2_id': 5221366095264680241}}", "('351788581', '452486137')": "{'path_nodes': ['351788581', '1614926340', '452486137'], 'path_polyline': '_mlyHvyVjAnBjAdB', 'origin': {'id': '351788581', 'x': 530408.2266209685, 'y': 181187.08346980734, 'lon': -0.12203698708857721, 'lat': 51.514564733241166, 's2_id': 5221366089140130631}, 'destination': {'id': '452486137', 'x': 530336.2029860646, 'y': 181100.6098553125, 'lon': -0.12310628708949857, 'lat': 51.51380423324127, 's2_id': 5221366089500926033}}", "('351788581', '1614978621')": "{'path_nodes': ['351788581', '21704017', '2441993346', '9475528', '455705622', '1614978621'], 'path_polyline': '_mlyHvyV{@y@CA}DmFgBmDE]', 'origin': {'id': '351788581', 'x': 530408.2266209685, 'y': 181187.08346980734, 'lon': -0.12203698708857721, 'lat': 51.514564733241166, 's2_id': 5221366089140130631}, 'destination': {'id': '1614978621', 'x': 530576.8274248724, 'y': 181393.9033903911, 'lon': -0.11953208708639783, 'lat': 51.5163844332409, 's2_id': 5221390732826561719}}", "('11863161', '60035532')": "{'path_nodes': ['11863161', '4360487620', '1204707018', '12689154', '14767049', '60035532'], 'path_polyline': 'q`nyHdqWM]Os@WoAc@eDqA}I', 'origin': {'id': '11863161', 'x': 530124.5688757555, 'y': 182097.73222469504, 'lon': -0.12578658708530252, 'lat': 51.522813733239765, 's2_id': 5221390715291547327}, 'destination': {'id': '60035532', 'x': 530356.7410048215, 'y': 182198.95950210653, 'lon': -0.12240458708331098, 'lat': 51.52366993323967, 's2_id': 5221390742020738093}}", "('3943984251', '107865')": "{'path_nodes': ['3943984251', '12689153', '107865'], 'path_polyline': 'uanyHpxVpBfNr@dC', 'origin': {'id': '3943984251', 'x': 530397.3803992561, 'y': 182123.92247810628, 'lon': -0.12184688708347477, 'lat': 51.522986233239784, 's2_id': 5221390741548489905}, 'destination': {'id': '107865', 'x': 530183.7334667961, 'y': 182026.20598506153, 'lon': -0.12496068708533102, 'lat': 51.52215733323993, 's2_id': 5221390715644839313}}", "('6849009938', '4347844152')": "{'path_nodes': ['6849009938', '2131643980', '108055', '4347844152'], 'path_polyline': 'qzkyHrzZgCgDeD}EEQ', 'origin': {'id': '6849009938', 'x': 528985.5627345478, 'y': 180821.66675852746, 'lon': -0.14266138709937176, 'lat': 51.5116071332414, 's2_id': 5221366497056720769}, 'destination': {'id': '4347844152', 'x': 529122.9852262605, 'y': 180996.59834250069, 'lon': -0.14061818709755844, 'lat': 51.51314783324118, 's2_id': 5221366132526926529}}", "('107851', '107848')": "{'path_nodes': ['107851', '14791174', '107848'], 'path_polyline': 'mhmyH~cXjCfIsCxC', 'origin': {'id': '107851', 'x': 529926.6551330115, 'y': 181662.51682727283, 'lon': -0.1287979870889234, 'lat': 51.518948133240364, 's2_id': 5221390719758694127}, 'destination': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}}", "('3085109046', '3943984250')": "{'path_nodes': ['3085109046', '3085109045', '319840216', '10703293', '5762492068', '5762492058', '108909', '12443172', '109826', '109827', '893428750', '25475744', '12026763', '3943984250'], 'path_polyline': 'kunyHjcUZh@l@`Ax@fAPPDDdEvHThA^hCt@jFF`@l@lEJv@f@nD', 'origin': {'id': '3085109046', 'x': 530978.6481179734, 'y': 182489.49950808723, 'lon': -0.11333758707787478, 'lat': 51.526137133239374, 's2_id': 5221390806464305045}, 'destination': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}}", "('1619527499', '5560599870')": "{'path_nodes': ['1619527499', '102003', '1619500464', '101995', '5560599870'], 'path_polyline': '{dmyHhr\\\\k@FyF^WBmCP', 'origin': {'id': '1619527499', 'x': 528348.6476746706, 'y': 181559.31827725726, 'lon': -0.15156608709925024, 'lat': 51.5183811332402, 's2_id': 5221390308991970271}, 'destination': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}}", "('1619527499', '1619527465')": "{'path_nodes': ['1619527499', '1619527487', '6859207542', '1619527484', '1619527465'], 'path_polyline': '{dmyHhr\\\\^CH?HA`@G', 'origin': {'id': '1619527499', 'x': 528348.6476746706, 'y': 181559.31827725726, 'lon': -0.15156608709925024, 'lat': 51.5183811332402, 's2_id': 5221390308991970271}, 'destination': {'id': '1619527465', 'x': 528354.5593254959, 'y': 181511.07415274787, 'lon': -0.15149848709947827, 'lat': 51.517946233240274, 's2_id': 5221390308953911399}}", "('1619527465', '1666324135')": "{'path_nodes': ['1619527465', '1667203543', '1666324195', '1666324135'], 'path_polyline': 'ebmyHzq\\\\v@IrC]zD_A', 'origin': {'id': '1619527465', 'x': 528354.5593254959, 'y': 181511.07415274787, 'lon': -0.15149848709947827, 'lat': 51.517946233240274, 's2_id': 5221390308953911399}, 'destination': {'id': '1666324135', 'x': 528396.2909888483, 'y': 181294.51926065335, 'lon': -0.15097618710041255, 'lat': 51.51599063324059, 's2_id': 5221390310473183493}}", "('1614978621', '351788581')": "{'path_nodes': ['1614978621', '455705622', '9475528', '2441993346', '21704017', '351788581'], 'path_polyline': 'kxlyH`jVD\\\\fBlD|DlFB@z@x@', 'origin': {'id': '1614978621', 'x': 530576.8274248724, 'y': 181393.9033903911, 'lon': -0.11953208708639783, 'lat': 51.5163844332409, 's2_id': 5221390732826561719}, 'destination': {'id': '351788581', 'x': 530408.2266209685, 'y': 181187.08346980734, 'lon': -0.12203698708857721, 'lat': 51.514564733241166, 's2_id': 5221366089140130631}}", "('1666324135', '1619527465')": "{'path_nodes': ['1666324135', '1666324195', '1667203543', '1619527465'], 'path_polyline': '}ulyHrn\\\\{D~@sC\\\\w@H', 'origin': {'id': '1666324135', 'x': 528396.2909888483, 'y': 181294.51926065335, 'lon': -0.15097618710041255, 'lat': 51.51599063324059, 's2_id': 5221390310473183493}, 'destination': {'id': '1619527465', 'x': 528354.5593254959, 'y': 181511.07415274787, 'lon': -0.15149848709947827, 'lat': 51.517946233240274, 's2_id': 5221390308953911399}}", "('1666324135', '9791503')": "{'path_nodes': ['1666324135', '25532841', '25532842', '5517018162', '9791503'], 'path_polyline': '}ulyHrn\\\\vBg@lAc@|@[tAo@', 'origin': {'id': '1666324135', 'x': 528396.2909888483, 'y': 181294.51926065335, 'lon': -0.15097618710041255, 'lat': 51.51599063324059, 's2_id': 5221390310473183493}, 'destination': {'id': '9791503', 'x': 528453.4017745669, 'y': 181103.33456195047, 'lon': -0.15022318710111182, 'lat': 51.514259533240875, 's2_id': 5221366509608678675}}", "('255566', '311422343')": "{'path_nodes': ['255566', '1684410076', '255564', '255562', '1684410105', '691084080', '107345', '107343', '102005', '311422343'], 'path_polyline': 'wrlyHjnZcAZ{@XkBl@}Ah@}Ah@eAZaAZ{Bz@_@N', 'origin': {'id': '255566', 'x': 529110.8165279367, 'y': 181255.83928485424, 'lon': -0.140698587096205, 'lat': 51.51548033324079, 's2_id': 5221390688019159091}, 'destination': {'id': '311422343', 'x': 528990.7943980636, 'y': 181651.66929955478, 'lon': -0.14228258709476566, 'lat': 51.5190649332402, 's2_id': 5221390324044305141}}", "('305691971', '3943984250')": "{'path_nodes': ['305691971', '281454972', '104305', '3943984250'], 'path_polyline': '}fnyHzxV_@{CFa@`CuA', 'origin': {'id': '305691971', 'x': 530391.2789650426, 'y': 182217.21618592896, 'lon': -0.12190028708299697, 'lat': 51.52382603323963, 's2_id': 5221390742060118677}, 'destination': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}}", "('10574891', '452486132')": "{'path_nodes': ['10574891', '1678452823', '1678452820', '1678452811', '1678452807', '9513062', '25496899', '2476787985', '25544138', '25544135', '452486132'], 'path_polyline': '{plyHhxWlGJBIRB@LxCXp@?~CKcCwI_AuB{DwG', 'origin': {'id': '10574891', 'x': 530066.6953816533, 'y': 181247.26636238, 'lon': -0.12693388709035136, 'lat': 51.51518423324101, 's2_id': 5221390723075322255}, 'destination': {'id': '452486132', 'x': 530316.2083825651, 'y': 181099.19575368112, 'lon': -0.12339478708962881, 'lat': 51.51379613324128, 's2_id': 5221366094895935055}}", "('5560599870', '1619527499')": "{'path_nodes': ['5560599870', '101995', '1619500464', '102003', '1619527499'], 'path_polyline': 'gsmyHft\\\\lCQVCxF_@j@G', 'origin': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}, 'destination': {'id': '1619527499', 'x': 528348.6476746706, 'y': 181559.31827725726, 'lon': -0.15156608709925024, 'lat': 51.5183811332402, 's2_id': 5221390308991970271}}", "('5560599870', '108208')": "{'path_nodes': ['5560599870', '1619316059', '1619316041', '108208'], 'path_polyline': 'gsmyHft\\\\M\\\\HfD@v@', 'origin': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}, 'destination': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}}", "('4347844152', '6849009938')": "{'path_nodes': ['4347844152', '108055', '2131643980', '6849009938'], 'path_polyline': 'edlyHzmZDPdD|EfCfD', 'origin': {'id': '4347844152', 'x': 529122.9852262605, 'y': 180996.59834250069, 'lon': -0.14061818709755844, 'lat': 51.51314783324118, 's2_id': 5221366132526926529}, 'destination': {'id': '6849009938', 'x': 528985.5627345478, 'y': 180821.66675852746, 'lon': -0.14266138709937176, 'lat': 51.5116071332414, 's2_id': 5221366497056720769}}", "('107848', '107842')": "{'path_nodes': ['107848', '6477034137', '107842'], 'path_polyline': 'uhmyH`sXGOaCsH', 'origin': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}, 'destination': {'id': '107842', 'x': 529869.4437073416, 'y': 181742.90904576948, 'lon': -0.1295924870888332, 'lat': 51.51968373324025, 's2_id': 5221390702256103647}}", "('3943984250', '305691971')": "{'path_nodes': ['3943984250', '104305', '281454972', '305691971'], 'path_polyline': 'scnyHfpVaCtAG`@^zC', 'origin': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}, 'destination': {'id': '305691971', 'x': 530391.2789650426, 'y': 182217.21618592896, 'lon': -0.12190028708299697, 'lat': 51.52382603323963, 's2_id': 5221390742060118677}}", "('3943984250', '3085109046')": "{'path_nodes': ['3943984250', '12026763', '25475744', '893428750', '109827', '109826', '12443172', '108909', '5762492058', '5762492068', '10703293', '319840216', '3085109045', '3085109046'], 'path_polyline': 'scnyHfpVg@oDKw@m@mEGa@u@kF_@iCUiAeEwHEEQQy@gAm@aA[i@', 'origin': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}, 'destination': {'id': '3085109046', 'x': 530978.6481179734, 'y': 182489.49950808723, 'lon': -0.11333758707787478, 'lat': 51.526137133239374, 's2_id': 5221390806464305045}}"} \ No newline at end of file +{"('14791189', '107848')": "{'path_nodes': ('14791189', '107848'), 'path_polyline': 'agmyHjvXs@iB', 'origin': {'id': '14791189', 'x': 529722.9713333722, 'y': 181633.29913677758, 'lon': -0.13174268709034131, 'lat': 51.5187323332404, 's2_id': 5221390701130112993}, 'destination': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}}", "('821550', '25522559')": "{'path_nodes': ('821550', '25522559'), 'path_polyline': 'icnyH|c[uAN', 'origin': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}, 'destination': {'id': '25522559', 'x': 528842.6948471027, 'y': 182161.95959961542, 'lon': -0.14422948709287078, 'lat': 51.523684533239404, 's2_id': 5221390328858015501}}", "('25522558', '821550')": "{'path_nodes': ('25522558', '821550'), 'path_polyline': '}dnyHfb[r@t@', 'origin': {'id': '25522558', 'x': 528867.1745911204, 'y': 182143.19631453254, 'lon': -0.14388368709282368, 'lat': 51.52351033323944, 's2_id': 5221390331520430839}, 'destination': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}}", "('1667203491', '1684410054')": "{'path_nodes': ('1667203491', '1684410054'), 'path_polyline': 'gxlyH`a\\\\~DsA', 'origin': {'id': '1667203491', 'x': 528545.5372283176, 'y': 181338.99680078932, 'lon': -0.14881028709924338, 'lat': 51.5163564332406, 's2_id': 5221390311898177595}, 'destination': {'id': '1684410054', 'x': 528577.228591008, 'y': 181232.8743807681, 'lon': -0.14839248709963276, 'lat': 51.5153955332407, 's2_id': 5221390312432500411}}", "('107848', '14791189')": "{'path_nodes': ('107848', '14791189'), 'path_polyline': 'uhmyH`sXr@hB', 'origin': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}, 'destination': {'id': '14791189', 'x': 529722.9713333722, 'y': 181633.29913677758, 'lon': -0.13174268709034131, 'lat': 51.5187323332404, 's2_id': 5221390701130112993}}", "('60035532', '60035535')": "{'path_nodes': ('60035532', '60035535'), 'path_polyline': '}enyH~{VF[', 'origin': {'id': '60035532', 'x': 530356.7410048215, 'y': 182198.95950210653, 'lon': -0.12240458708331098, 'lat': 51.52366993323967, 's2_id': 5221390742020738093}, 'destination': {'id': '60035535', 'x': 530366.8640776458, 'y': 182194.5456432234, 'lon': -0.12226038708327418, 'lat': 51.523627933239666, 's2_id': 5221390742017931799}}", "('305691971', '109349')": "{'path_nodes': ('305691971', '109349'), 'path_polyline': '}fnyHzxVVdA', 'origin': {'id': '305691971', 'x': 530391.2789650426, 'y': 182217.21618592896, 'lon': -0.12190028708299697, 'lat': 51.52382603323963, 's2_id': 5221390742060118677}, 'destination': {'id': '109349', 'x': 530367.4912101941, 'y': 182203.92011985858, 'lon': -0.12224788708321918, 'lat': 51.52371203323968, 's2_id': 5221390742034218987}}", "('60035535', '3943984251')": "{'path_nodes': ('60035535', '3943984251'), 'path_polyline': 'uenyHb{V~BqA', 'origin': {'id': '60035535', 'x': 530366.8640776458, 'y': 182194.5456432234, 'lon': -0.12226038708327418, 'lat': 51.523627933239666, 's2_id': 5221390742017931799}, 'destination': {'id': '3943984251', 'x': 530397.3803992561, 'y': 182123.92247810628, 'lon': -0.12184688708347477, 'lat': 51.522986233239784, 's2_id': 5221390741548489905}}", "('109349', '60035535')": "{'path_nodes': ('109349', '60035535'), 'path_polyline': 'efnyH`{VN@', 'origin': {'id': '109349', 'x': 530367.4912101941, 'y': 182203.92011985858, 'lon': -0.12224788708321918, 'lat': 51.52371203323968, 's2_id': 5221390742034218987}, 'destination': {'id': '60035535', 'x': 530366.8640776458, 'y': 182194.5456432234, 'lon': -0.12226038708327418, 'lat': 51.523627933239666, 's2_id': 5221390742017931799}}", "('1684410054', '1684410058')": "{'path_nodes': ('1684410054', '1684410058'), 'path_polyline': 'grlyHl~[Y_D', 'origin': {'id': '1684410054', 'x': 528577.228591008, 'y': 181232.8743807681, 'lon': -0.14839248709963276, 'lat': 51.5153955332407, 's2_id': 5221390312432500411}, 'destination': {'id': '1684410058', 'x': 528632.2359216934, 'y': 181249.06604453287, 'lon': -0.1475942870992026, 'lat': 51.515528533240705, 's2_id': 5221390312863352989}}", "('2047806084', '255566')": "{'path_nodes': ('2047806084', '255566'), 'path_polyline': 'krlyHdnZKD', 'origin': {'id': '2047806084', 'x': 529113.1043697781, 'y': 181249.41004932253, 'lon': -0.14066798709622622, 'lat': 51.51542203324082, 's2_id': 5221390688018024529}, 'destination': {'id': '255566', 'x': 529110.8165279367, 'y': 181255.83928485424, 'lon': -0.140698587096205, 'lat': 51.51548033324079, 's2_id': 5221390688019159091}}", "('60035532', '60035533')": "{'path_nodes': ('60035532', '60035533'), 'path_polyline': '}enyH~{VOK', 'origin': {'id': '60035532', 'x': 530356.7410048215, 'y': 182198.95950210653, 'lon': -0.12240458708331098, 'lat': 51.52366993323967, 's2_id': 5221390742020738093}, 'destination': {'id': '60035533', 'x': 530360.7288168034, 'y': 182208.29780931148, 'lon': -0.12234368708323559, 'lat': 51.52375293323967, 's2_id': 5221390742025021247}}", "('1684410058', '1684410054')": "{'path_nodes': ('1684410058', '1684410054'), 'path_polyline': 'aslyHly[X~C', 'origin': {'id': '1684410058', 'x': 528632.2359216934, 'y': 181249.06604453287, 'lon': -0.1475942870992026, 'lat': 51.515528533240705, 's2_id': 5221390312863352989}, 'destination': {'id': '1684410054', 'x': 528577.228591008, 'y': 181232.8743807681, 'lon': -0.14839248709963276, 'lat': 51.5153955332407, 's2_id': 5221390312432500411}}", "('821559', '821550')": "{'path_nodes': ('821559', '821550'), 'path_polyline': '}}myHna[kDlA', 'origin': {'id': '821559', 'x': 528878.7128509199, 'y': 182019.1831767487, 'lon': -0.1437627870934339, 'lat': 51.52239323323963, 's2_id': 5221390325857124155}, 'destination': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}}", "('821550', '821559')": "{'path_nodes': ('821550', '821559'), 'path_polyline': 'icnyH|c[jDmA', 'origin': {'id': '821550', 'x': 528849.3511895654, 'y': 182113.8126146983, 'lon': -0.1441511870930946, 'lat': 51.523250333239496, 's2_id': 5221390328818504945}, 'destination': {'id': '821559', 'x': 528878.7128509199, 'y': 182019.1831767487, 'lon': -0.1437627870934339, 'lat': 51.52239323323963, 's2_id': 5221390325857124155}}", "('109349', '60035533')": "{'path_nodes': ('109349', '60035533'), 'path_polyline': 'efnyH`{VGP', 'origin': {'id': '109349', 'x': 530367.4912101941, 'y': 182203.92011985858, 'lon': -0.12224788708321918, 'lat': 51.52371203323968, 's2_id': 5221390742034218987}, 'destination': {'id': '60035533', 'x': 530360.7288168034, 'y': 182208.29780931148, 'lon': -0.12234368708323559, 'lat': 51.52375293323967, 's2_id': 5221390742025021247}}", "('60035533', '305691975')": "{'path_nodes': ('60035533', '305691975'), 'path_polyline': 'mfnyHr{Vk@J', 'origin': {'id': '60035533', 'x': 530360.7288168034, 'y': 182208.29780931148, 'lon': -0.12234368708323559, 'lat': 51.52375293323967, 's2_id': 5221390742025021247}, 'destination': {'id': '305691975', 'x': 530356.2034866889, 'y': 182232.3510177004, 'lon': -0.12239998708313073, 'lat': 51.52397013323962, 's2_id': 5221390741976307405}}", "('351788581', '452486137')": "{'path_nodes': ['351788581', '1614926340', '452486137'], 'path_polyline': '_mlyHvyVjAnBjAdB', 'origin': {'id': '351788581', 'x': 530408.2266209685, 'y': 181187.08346980734, 'lon': -0.12203698708857721, 'lat': 51.514564733241166, 's2_id': 5221366089140130631}, 'destination': {'id': '452486137', 'x': 530336.2029860646, 'y': 181100.6098553125, 'lon': -0.12310628708949857, 'lat': 51.51380423324127, 's2_id': 5221366089500926033}}", "('351788581', '1614978621')": "{'path_nodes': ['351788581', '21704017', '2441993346', '9475528', '455705622', '1614978621'], 'path_polyline': '_mlyHvyV{@y@CA}DmFgBmDE]', 'origin': {'id': '351788581', 'x': 530408.2266209685, 'y': 181187.08346980734, 'lon': -0.12203698708857721, 'lat': 51.514564733241166, 's2_id': 5221366089140130631}, 'destination': {'id': '1614978621', 'x': 530576.8274248724, 'y': 181393.9033903911, 'lon': -0.11953208708639783, 'lat': 51.5163844332409, 's2_id': 5221390732826561719}}", "('305691971', '3943984250')": "{'path_nodes': ['305691971', '281454972', '104305', '3943984250'], 'path_polyline': '}fnyHzxV_@{CFa@`CuA', 'origin': {'id': '305691971', 'x': 530391.2789650426, 'y': 182217.21618592896, 'lon': -0.12190028708299697, 'lat': 51.52382603323963, 's2_id': 5221390742060118677}, 'destination': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}}", "('107843', '107842')": "{'path_nodes': ['107843', '4860880474', '107842'], 'path_polyline': 'oomyHdcX`AvCL^', 'origin': {'id': '107843', 'x': 529932.1864720277, 'y': 181788.54521884263, 'lon': -0.12867188708819463, 'lat': 51.520079433240205, 's2_id': 5221390719594778311}, 'destination': {'id': '107842', 'x': 529869.4437073416, 'y': 181742.90904576948, 'lon': -0.1295924870888332, 'lat': 51.51968373324025, 's2_id': 5221390702256103647}}", "('107843', '108014')": "{'path_nodes': ['107843', '983836443', '108014'], 'path_polyline': 'oomyHdcXaAyBmBwF', 'origin': {'id': '107843', 'x': 529932.1864720277, 'y': 181788.54521884263, 'lon': -0.12867188708819463, 'lat': 51.520079433240205, 's2_id': 5221390719594778311}, 'destination': {'id': '108014', 'x': 530058.0320436077, 'y': 181889.4880572388, 'lon': -0.12682188708686193, 'lat': 51.520957633240066, 's2_id': 5221390717852326699}}", "('3943984251', '107865')": "{'path_nodes': ['3943984251', '12689153', '107865'], 'path_polyline': 'uanyHpxVpBfNr@dC', 'origin': {'id': '3943984251', 'x': 530397.3803992561, 'y': 182123.92247810628, 'lon': -0.12184688708347477, 'lat': 51.522986233239784, 's2_id': 5221390741548489905}, 'destination': {'id': '107865', 'x': 530183.7334667961, 'y': 182026.20598506153, 'lon': -0.12496068708533102, 'lat': 51.52215733323993, 's2_id': 5221390715644839313}}", "('11863161', '60035532')": "{'path_nodes': ['11863161', '4360487620', '1204707018', '12689154', '14767049', '60035532'], 'path_polyline': 'q`nyHdqWM]Os@WoAc@eDqA}I', 'origin': {'id': '11863161', 'x': 530124.5688757555, 'y': 182097.73222469504, 'lon': -0.12578658708530252, 'lat': 51.522813733239765, 's2_id': 5221390715291547327}, 'destination': {'id': '60035532', 'x': 530356.7410048215, 'y': 182198.95950210653, 'lon': -0.12240458708331098, 'lat': 51.52366993323967, 's2_id': 5221390742020738093}}", "('6849009938', '4347844152')": "{'path_nodes': ['6849009938', '2131643980', '108055', '4347844152'], 'path_polyline': 'qzkyHrzZgCgDeD}EEQ', 'origin': {'id': '6849009938', 'x': 528985.5627345478, 'y': 180821.66675852746, 'lon': -0.14266138709937176, 'lat': 51.5116071332414, 's2_id': 5221366497056720769}, 'destination': {'id': '4347844152', 'x': 529122.9852262605, 'y': 180996.59834250069, 'lon': -0.14061818709755844, 'lat': 51.51314783324118, 's2_id': 5221366132526926529}}", "('1616122178', '452486132')": "{'path_nodes': ['1616122178', '1616122145', '452486132'], 'path_polyline': 'cmlyHpiWv@cAbBgD', 'origin': {'id': '1616122178', 'x': 530232.2897525224, 'y': 181184.16290962603, 'lon': -0.12457208708967878, 'lat': 51.51457903324111, 's2_id': 5221366095264680241}, 'destination': {'id': '452486132', 'x': 530316.2083825651, 'y': 181099.19575368112, 'lon': -0.12339478708962881, 'lat': 51.51379613324128, 's2_id': 5221366094895935055}}", "('1666324135', '1619527465')": "{'path_nodes': ['1666324135', '1666324195', '1667203543', '1619527465'], 'path_polyline': '}ulyHrn\\\\{D~@sC\\\\w@H', 'origin': {'id': '1666324135', 'x': 528396.2909888483, 'y': 181294.51926065335, 'lon': -0.15097618710041255, 'lat': 51.51599063324059, 's2_id': 5221390310473183493}, 'destination': {'id': '1619527465', 'x': 528354.5593254959, 'y': 181511.07415274787, 'lon': -0.15149848709947827, 'lat': 51.517946233240274, 's2_id': 5221390308953911399}}", "('1666324135', '9791503')": "{'path_nodes': ['1666324135', '25532841', '25532842', '5517018162', '9791503'], 'path_polyline': '}ulyHrn\\\\vBg@lAc@|@[tAo@', 'origin': {'id': '1666324135', 'x': 528396.2909888483, 'y': 181294.51926065335, 'lon': -0.15097618710041255, 'lat': 51.51599063324059, 's2_id': 5221390310473183493}, 'destination': {'id': '9791503', 'x': 528453.4017745669, 'y': 181103.33456195047, 'lon': -0.15022318710111182, 'lat': 51.514259533240875, 's2_id': 5221366509608678675}}", "('1684410058', '108252')": "{'path_nodes': ['1684410058', '108239', '25257028', '25256949', '168272', '108252'], 'path_polyline': 'aslyHly[pDgCdBiB\\\\a@tC{CxDaE', 'origin': {'id': '1684410058', 'x': 528632.2359216934, 'y': 181249.06604453287, 'lon': -0.1475942870992026, 'lat': 51.515528533240705, 's2_id': 5221390312863352989}, 'destination': {'id': '108252', 'x': 528858.8179076333, 'y': 180895.9103971371, 'lon': -0.14445968709974794, 'lat': 51.512303233241255, 's2_id': 5221366500350297403}}", "('1619527499', '5560599870')": "{'path_nodes': ['1619527499', '102003', '1619500464', '101995', '5560599870'], 'path_polyline': '{dmyHhr\\\\k@FyF^WBmCP', 'origin': {'id': '1619527499', 'x': 528348.6476746706, 'y': 181559.31827725726, 'lon': -0.15156608709925024, 'lat': 51.5183811332402, 's2_id': 5221390308991970271}, 'destination': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}}", "('1619527499', '1619527465')": "{'path_nodes': ['1619527499', '1619527487', '6859207542', '1619527484', '1619527465'], 'path_polyline': '{dmyHhr\\\\^CH?HA`@G', 'origin': {'id': '1619527499', 'x': 528348.6476746706, 'y': 181559.31827725726, 'lon': -0.15156608709925024, 'lat': 51.5183811332402, 's2_id': 5221390308991970271}, 'destination': {'id': '1619527465', 'x': 528354.5593254959, 'y': 181511.07415274787, 'lon': -0.15149848709947827, 'lat': 51.517946233240274, 's2_id': 5221390308953911399}}", "('107848', '107842')": "{'path_nodes': ['107848', '6477034137', '107842'], 'path_polyline': 'uhmyH`sXGOaCsH', 'origin': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}, 'destination': {'id': '107842', 'x': 529869.4437073416, 'y': 181742.90904576948, 'lon': -0.1295924870888332, 'lat': 51.51968373324025, 's2_id': 5221390702256103647}}", "('1614978621', '351788581')": "{'path_nodes': ['1614978621', '455705622', '9475528', '2441993346', '21704017', '351788581'], 'path_polyline': 'kxlyH`jVD\\\\fBlD|DlFB@z@x@', 'origin': {'id': '1614978621', 'x': 530576.8274248724, 'y': 181393.9033903911, 'lon': -0.11953208708639783, 'lat': 51.5163844332409, 's2_id': 5221390732826561719}, 'destination': {'id': '351788581', 'x': 530408.2266209685, 'y': 181187.08346980734, 'lon': -0.12203698708857721, 'lat': 51.514564733241166, 's2_id': 5221366089140130631}}", "('5560599870', '1619527499')": "{'path_nodes': ['5560599870', '101995', '1619500464', '102003', '1619527499'], 'path_polyline': 'gsmyHft\\\\lCQVCxF_@j@G', 'origin': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}, 'destination': {'id': '1619527499', 'x': 528348.6476746706, 'y': 181559.31827725726, 'lon': -0.15156608709925024, 'lat': 51.5183811332402, 's2_id': 5221390308991970271}}", "('5560599870', '108208')": "{'path_nodes': ['5560599870', '1619316059', '1619316041', '108208'], 'path_polyline': 'gsmyHft\\\\M\\\\HfD@v@', 'origin': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}, 'destination': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}}", "('3085109046', '3943984250')": "{'path_nodes': ['3085109046', '3085109045', '319840216', '10703293', '5762492068', '5762492058', '108909', '12443172', '109826', '109827', '893428750', '25475744', '12026763', '3943984250'], 'path_polyline': 'kunyHjcUZh@l@`Ax@fAPPDDdEvHThA^hCt@jFF`@l@lEJv@f@nD', 'origin': {'id': '3085109046', 'x': 530978.6481179734, 'y': 182489.49950808723, 'lon': -0.11333758707787478, 'lat': 51.526137133239374, 's2_id': 5221390806464305045}, 'destination': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}}", "('3943984250', '305691971')": "{'path_nodes': ['3943984250', '104305', '281454972', '305691971'], 'path_polyline': 'scnyHfpVaCtAG`@^zC', 'origin': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}, 'destination': {'id': '305691971', 'x': 530391.2789650426, 'y': 182217.21618592896, 'lon': -0.12190028708299697, 'lat': 51.52382603323963, 's2_id': 5221390742060118677}}", "('3943984250', '3085109046')": "{'path_nodes': ['3943984250', '12026763', '25475744', '893428750', '109827', '109826', '12443172', '108909', '5762492058', '5762492068', '10703293', '319840216', '3085109045', '3085109046'], 'path_polyline': 'scnyHfpVg@oDKw@m@mEGa@u@kF_@iCUiAeEwHEEQQy@gAm@aA[i@', 'origin': {'id': '3943984250', 'x': 530488.4049059168, 'y': 182160.76664384146, 'lon': -0.12052198708270978, 'lat': 51.523296333239756, 's2_id': 5221390741138110379}, 'destination': {'id': '3085109046', 'x': 530978.6481179734, 'y': 182489.49950808723, 'lon': -0.11333758707787478, 'lat': 51.526137133239374, 's2_id': 5221390806464305045}}", "('255566', '311422343')": "{'path_nodes': ['255566', '1684410076', '255564', '255562', '1684410105', '691084080', '107345', '107343', '102005', '311422343'], 'path_polyline': 'wrlyHjnZcAZ{@XkBl@}Ah@}Ah@eAZaAZ{Bz@_@N', 'origin': {'id': '255566', 'x': 529110.8165279367, 'y': 181255.83928485424, 'lon': -0.140698587096205, 'lat': 51.51548033324079, 's2_id': 5221390688019159091}, 'destination': {'id': '311422343', 'x': 528990.7943980636, 'y': 181651.66929955478, 'lon': -0.14228258709476566, 'lat': 51.5190649332402, 's2_id': 5221390324044305141}}", "('107851', '107848')": "{'path_nodes': ['107851', '14791174', '107848'], 'path_polyline': 'mhmyH~cXjCfIsCxC', 'origin': {'id': '107851', 'x': 529926.6551330115, 'y': 181662.51682727283, 'lon': -0.1287979870889234, 'lat': 51.518948133240364, 's2_id': 5221390719758694127}, 'destination': {'id': '107848', 'x': 529759.3082895541, 'y': 181663.1699073442, 'lon': -0.13120828708995377, 'lat': 51.51899243324034, 's2_id': 5221390701409839275}}", "('311422343', '255566')": "{'path_nodes': ['311422343', '102005', '107343', '107345', '691084080', '1684410105', '255562', '255564', '1684410076', '255566'], 'path_polyline': 'cimyHfxZ^OzB{@`A[dA[|Ai@|Ai@jBm@z@YbA[', 'origin': {'id': '311422343', 'x': 528990.7943980636, 'y': 181651.66929955478, 'lon': -0.14228258709476566, 'lat': 51.5190649332402, 's2_id': 5221390324044305141}, 'destination': {'id': '255566', 'x': 529110.8165279367, 'y': 181255.83928485424, 'lon': -0.140698587096205, 'lat': 51.51548033324079, 's2_id': 5221390688019159091}}", "('110008', '108208')": "{'path_nodes': ['110008', '1612319339', '1612319349', '108209', '1610964470', '1667118171', '108208'], 'path_polyline': 'ypmyHhr]CWQaBMsAe@uIAiACuA', 'origin': {'id': '110008', 'x': 527987.5162014617, 'y': 181762.74365661351, 'lon': -0.1566941871003674, 'lat': 51.520291133239844, 's2_id': 5221390292694281737}, 'destination': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}}", "('452486132', '1616122178')": "{'path_nodes': ['452486132', '1616122145', '1616122178'], 'path_polyline': 'ghlyHdbWcBfDw@bA', 'origin': {'id': '452486132', 'x': 530316.2083825651, 'y': 181099.19575368112, 'lon': -0.12339478708962881, 'lat': 51.51379613324128, 's2_id': 5221366094895935055}, 'destination': {'id': '1616122178', 'x': 530232.2897525224, 'y': 181184.16290962603, 'lon': -0.12457208708967878, 'lat': 51.51457903324111, 's2_id': 5221366095264680241}}", "('4347844152', '6849009938')": "{'path_nodes': ['4347844152', '108055', '2131643980', '6849009938'], 'path_polyline': 'edlyHzmZDPdD|EfCfD', 'origin': {'id': '4347844152', 'x': 529122.9852262605, 'y': 180996.59834250069, 'lon': -0.14061818709755844, 'lat': 51.51314783324118, 's2_id': 5221366132526926529}, 'destination': {'id': '6849009938', 'x': 528985.5627345478, 'y': 180821.66675852746, 'lon': -0.14266138709937176, 'lat': 51.5116071332414, 's2_id': 5221366497056720769}}", "('305691975', '107877')": "{'path_nodes': ['305691975', '109348', '110269', '110270', '107877'], 'path_polyline': 'ygnyH~{VyC~AiCpAeCpAaD|A', 'origin': {'id': '305691975', 'x': 530356.2034866889, 'y': 182232.3510177004, 'lon': -0.12239998708313073, 'lat': 51.52397013323962, 's2_id': 5221390741976307405}, 'destination': {'id': '107877', 'x': 530225.3073599826, 'y': 182556.42574277206, 'lon': -0.12416598708215254, 'lat': 51.5269126332391, 's2_id': 5221390754930610347}}", "('107877', '305691975')": "{'path_nodes': ['107877', '110270', '110269', '109348', '305691975'], 'path_polyline': 'eznyH`gW`D}AdCqAhCqAxC_B', 'origin': {'id': '107877', 'x': 530225.3073599826, 'y': 182556.42574277206, 'lon': -0.12416598708215254, 'lat': 51.5269126332391, 's2_id': 5221390754930610347}, 'destination': {'id': '305691975', 'x': 530356.2034866889, 'y': 182232.3510177004, 'lon': -0.12239998708313073, 'lat': 51.52397013323962, 's2_id': 5221390741976307405}}", "('108208', '110008')": "{'path_nodes': ['108208', '1667118171', '1610964470', '108209', '1612319349', '1612319339', '110008'], 'path_polyline': 'ismyHd|\\\\BtA@hAd@tILrAP`BBV', 'origin': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}, 'destination': {'id': '110008', 'x': 527987.5162014617, 'y': 181762.74365661351, 'lon': -0.1566941871003674, 'lat': 51.520291133239844, 's2_id': 5221390292694281737}}", "('108208', '5560599870')": "{'path_nodes': ['108208', '6342450111', '108210', '1667118184', '5560616885', '25500809', '21651765', '108212', '5560599870'], 'path_polyline': 'ismyHd|\\\\OYkBJAkBCqBFGv@?h@CXE', 'origin': {'id': '108208', 'x': 528232.1916799454, 'y': 181813.44509077538, 'lon': -0.15315118709857323, 'lat': 51.5206913332398, 's2_id': 5221390290123976625}, 'destination': {'id': '5560599870', 'x': 528320.2692946942, 'y': 181814.06644076837, 'lon': -0.15188218709802234, 'lat': 51.52067693323983, 's2_id': 5221390306965404305}}", "('1619527465', '1666324135')": "{'path_nodes': ['1619527465', '1667203543', '1666324195', '1666324135'], 'path_polyline': 'ebmyHzq\\\\v@IrC]zD_A', 'origin': {'id': '1619527465', 'x': 528354.5593254959, 'y': 181511.07415274787, 'lon': -0.15149848709947827, 'lat': 51.517946233240274, 's2_id': 5221390308953911399}, 'destination': {'id': '1666324135', 'x': 528396.2909888483, 'y': 181294.51926065335, 'lon': -0.15097618710041255, 'lat': 51.51599063324059, 's2_id': 5221390310473183493}}", "('10574891', '452486132')": "{'path_nodes': ['10574891', '1678452823', '1678452820', '1678452811', '1678452807', '9513062', '25496899', '2476787985', '25544138', '25544135', '452486132'], 'path_polyline': '{plyHhxWlGJBIRB@LxCXp@?~CKcCwI_AuB{DwG', 'origin': {'id': '10574891', 'x': 530066.6953816533, 'y': 181247.26636238, 'lon': -0.12693388709035136, 'lat': 51.51518423324101, 's2_id': 5221390723075322255}, 'destination': {'id': '452486132', 'x': 530316.2083825651, 'y': 181099.19575368112, 'lon': -0.12339478708962881, 'lat': 51.51379613324128, 's2_id': 5221366094895935055}}", "('4356572322', '1616122178')": "{'path_nodes': ['4356572322', '1616122237', '1616122178'], 'path_polyline': 'qtlyHtsWzDqFp@qA', 'origin': {'id': '4356572322', 'x': 530116.5515434985, 'y': 181313.1831658283, 'lon': -0.12619148708967906, 'lat': 51.515765133240926, 's2_id': 5221390724501967751}, 'destination': {'id': '1616122178', 'x': 530232.2897525224, 'y': 181184.16290962603, 'lon': -0.12457208708967878, 'lat': 51.51457903324111, 's2_id': 5221366095264680241}}", "('821559', '311422343')": "{'path_nodes': ['821559', '5560625853', '1694551560', '1685938656', '822403', '102000', '311422343'], 'path_polyline': '}}myHna[x@YVIdC}@dBq@hC{@pEwA', 'origin': {'id': '821559', 'x': 528878.7128509199, 'y': 182019.1831767487, 'lon': -0.1437627870934339, 'lat': 51.52239323323963, 's2_id': 5221390325857124155}, 'destination': {'id': '311422343', 'x': 528990.7943980636, 'y': 181651.66929955478, 'lon': -0.14228258709476566, 'lat': 51.5190649332402, 's2_id': 5221390324044305141}}"} \ No newline at end of file diff --git a/genet/core.py b/genet/core.py index 28fbe598..57e6b9d0 100644 --- a/genet/core.py +++ b/genet/core.py @@ -25,7 +25,7 @@ import genet.utils.plot as plot import genet.utils.simplification as simplification import genet.utils.spatial as spatial -import genet.validate.network_validation as network_validation +import genet.validate.network as network_validation import geopandas as gpd import networkx as nx import numpy as np @@ -942,7 +942,7 @@ def subgraph_on_link_conditions(self, conditions, how=any, mixed_dtypes=True): def modes(self): """ - Scans network for 'modes' attribute and returns list of all modes present int he network + Scans network for 'modes' attribute and returns list of all modes present in the network :return: """ modes = set() @@ -1999,55 +1999,60 @@ def invalid_network_routes(self): return [route.id for route in self.schedule.routes() if not route.has_network_route() or not self.is_valid_network_route(route)] - def generate_validation_report(self, link_length_threshold=1000): + def generate_validation_report(self, modes_for_strong_connectivity=None, link_metre_length_threshold=1000): """ Generates a dictionary with keys: 'graph', 'schedule' and 'routing' describing validity of the Network's underlying graph, the schedule services and then the intersection of the two which is the routing of schedule services onto the graph. - :param link_length_threshold: in meters defaults to 1000, i.e. 1km + :param modes_for_strong_connectivity: list of modes in the network that need to be checked for strong + connectivity. Defaults to 'car', 'walk' and 'bike' + :param link_metre_length_threshold: in meters defaults to 1000, i.e. 1km :return: """ logging.info('Checking validity of the Network') logging.info('Checking validity of the Network graph') report = {} - # describe network connectivity - modes = ['car', 'walk', 'bike'] - report['graph'] = {'graph_connectivity': {}} - for mode in modes: - logging.info(f'Checking network connectivity for mode: {mode}') - # subgraph for the mode to be tested - G_mode = self.modal_subgraph(mode) - # calculate how many connected subgraphs there are - report['graph']['graph_connectivity'][mode] = network_validation.describe_graph_connectivity(G_mode) - - def links_over_threshold_length(value): - return value >= link_length_threshold - - links_over_1km_length = self.extract_links_on_edge_attributes( - conditions={'length': links_over_threshold_length}) + # describe network connectivity + if modes_for_strong_connectivity is None: + modes_for_strong_connectivity = ['car', 'walk', 'bike'] + logging.info(f'Defaulting to checking graph connectivity for modes: {modes_for_strong_connectivity}. ' + 'You can change this by passing a `modes_for_strong_connectivity` param') + graph_connectivity = {} + for mode in modes_for_strong_connectivity: + graph_connectivity[mode] = self.check_connectivity_for_mode(mode) + report['graph'] = {'graph_connectivity': graph_connectivity} + + # attribute checks + conditions_toolbox = network_validation.ConditionsToolbox() report['graph']['link_attributes'] = { - 'links_over_1km_length': { - 'number_of': len(links_over_1km_length), - 'percentage': len(links_over_1km_length) / self.graph.number_of_edges(), - 'link_ids': links_over_1km_length - } - } + f'{k}_attributes': {} for k in conditions_toolbox.condition_names()} - def zero_value(value): - return (value == 0) or (value == '0') or (value == '0.0') - - report['graph']['link_attributes']['zero_attributes'] = {} - for attrib in [d.name for d in graph_operations.get_attribute_schema(self.links()).descendants]: - links_with_zero_attrib = self.extract_links_on_edge_attributes( - conditions={attrib: zero_value}, mixed_dtypes=False) - if links_with_zero_attrib: - logging.warning(f'{len(links_with_zero_attrib)} of links have values of 0 for `{attrib}`') - report['graph']['link_attributes']['zero_attributes'][attrib] = { - 'number_of': len(links_with_zero_attrib), - 'percentage': len(links_with_zero_attrib) / self.graph.number_of_edges(), - 'link_ids': links_with_zero_attrib - } + # checks on length attribute specifically + def links_over_threshold_length(value): + return value >= link_metre_length_threshold + + report['graph']['link_attributes']['links_over_1000_length'] = self.report_on_link_attribute_condition( + 'length', links_over_threshold_length) + + # more general attribute value checks + non_testable = ['id', 'from', 'to', 's2_to', 's2_from', 'geometry'] + link_attributes = [graph_operations.parse_leaf(leaf) for leaf in + graph_operations.get_attribute_schema(self.links()).leaves] + link_attributes = [attrib for attrib in link_attributes if attrib not in non_testable] + for attrib in link_attributes: + logging.info(f'Checking link values for `{attrib}`') + for condition_name in conditions_toolbox.condition_names(): + links_satifying_condition = self.report_on_link_attribute_condition( + attrib, conditions_toolbox.get_condition_evaluator(condition_name)) + if links_satifying_condition['number_of']: + logging.warning( + f'{links_satifying_condition["number_of"]} of links have ' + f'{condition_name} values for `{attrib}`') + if isinstance(attrib, dict): + attrib = dict_support.dict_to_string(attrib) + report['graph']['link_attributes'][f'{condition_name}_attributes'][ + attrib] = links_satifying_condition if self.schedule: report['schedule'] = self.schedule.generate_validation_report() @@ -2066,6 +2071,39 @@ def zero_value(value): } return report + def report_on_link_attribute_condition(self, attribute, condition): + """ + :param attribute: one of the link attributes, e.g. 'length' + :param condition: callable, condition for link[attribute] to satisfy + :return: + """ + if isinstance(attribute, dict): + conditions = dict_support.nest_at_leaf(deepcopy(attribute), condition) + else: + conditions = {attribute: condition} + + links_satifying_condition = self.extract_links_on_edge_attributes(conditions=conditions) + return { + 'number_of': len(links_satifying_condition), + 'percentage': len(links_satifying_condition) / self.graph.number_of_edges(), + 'link_ids': links_satifying_condition + } + + def check_connectivity_for_mode(self, mode): + logging.info(f'Checking network connectivity for mode: {mode}') + G_mode = self.modal_subgraph(mode) + con_desc = network_validation.describe_graph_connectivity(G_mode) + no_of_components = con_desc["number_of_connected_subgraphs"] + logging.info(f'The graph for mode: {mode} has: ' + f'{no_of_components} connected components, ' + f'{len(con_desc["problem_nodes"]["dead_ends"])} sinks/dead_ends and ' + f'{len(con_desc["problem_nodes"]["unreachable_node"])} sources/unreachable nodes.') + if no_of_components > 1: + logging.warning(f'The graph has more than one connected component for mode {mode}! ' + 'If this is not expected, consider using the `connect_components` method to connect the ' + 'components, or `retain_n_connected_subgraphs` with `n=1` to extract the largest component') + return con_desc + def generate_standard_outputs(self, output_dir, gtfs_day='19700101', include_shp_files=False): """ Generates geojsons that can be used for generating standard kepler visualisations. diff --git a/genet/output/matsim_xml_writer.py b/genet/output/matsim_xml_writer.py index f4b211ce..a779bebc 100644 --- a/genet/output/matsim_xml_writer.py +++ b/genet/output/matsim_xml_writer.py @@ -4,7 +4,7 @@ from copy import deepcopy from pandas import DataFrame from genet.output import sanitiser -from genet.validate.network_validation import validate_attribute_data +from genet.validate.network import validate_attribute_data from genet.utils.spatial import encode_shapely_linestring_to_polyline from genet.exceptions import MalformedAdditionalAttributeError import genet.variables as variables diff --git a/genet/schedule_elements.py b/genet/schedule_elements.py index 5bcd0d0b..d79ec67e 100644 --- a/genet/schedule_elements.py +++ b/genet/schedule_elements.py @@ -23,7 +23,7 @@ import genet.utils.persistence as persistence import genet.utils.plot as plot import genet.utils.spatial as spatial -import genet.validate.schedule_validation as schedule_validation +import genet.validate.schedule as schedule_validation import networkx as nx import numpy as np import pandas as pd diff --git a/genet/utils/dict_support.py b/genet/utils/dict_support.py index e0cd0718..9a232677 100644 --- a/genet/utils/dict_support.py +++ b/genet/utils/dict_support.py @@ -113,6 +113,10 @@ def combine_edge_data_lists(l1, l2): return [(u, v, dat) for (u, v), dat in edges.items()] +def dict_to_string(d): + return str(d).replace('{', '').replace('}', '').replace("'", '').replace(' ', ':') + + def dataframe_to_dict(df): return {_id: {k: v for k, v in m.items() if notna(v)} for _id, m in df.to_dict().items()} diff --git a/genet/utils/graph_operations.py b/genet/utils/graph_operations.py index 23318ed6..d92d5c79 100644 --- a/genet/utils/graph_operations.py +++ b/genet/utils/graph_operations.py @@ -6,6 +6,7 @@ from anytree import Node, RenderTree from genet.utils import pandas_helpers as pd_helpers +import genet.utils.dict_support as dict_support class Filter: @@ -209,6 +210,21 @@ def render_tree(root, data=False): print("%s%s" % (pre, node.name)) +def parse_leaf(leaf): + """ + :param leaf: anytree.node.node.Node + :return: str or dictionary with string key value pairs, for use as keys to extraction methods + """ + if leaf.depth > 1: + dict_path = {leaf.path[1].name: leaf.path[2].name} + if leaf.depth > 2: + for node in leaf.path[3:]: + dict_path = dict_support.nest_at_leaf(dict_path, node.name) + return dict_path + else: + return leaf.name + + def get_attribute_data_under_key(iterator: Iterable, key: Union[str, dict]): """ Returns all data stored under key in attribute dictionaries for iterators yielding (index, attribute_dictionary), @@ -256,8 +272,7 @@ def build_attribute_dataframe(iterator, keys: Union[list, str], index_name: str for key in keys: if isinstance(key, dict): # consolidate nestedness to get a name for the column - name = str(key) - name = name.replace('{', '').replace('}', '').replace("'", '').replace(' ', ':') + name = dict_support.dict_to_string(key) else: name = key diff --git a/genet/validate/network.py b/genet/validate/network.py new file mode 100644 index 00000000..60f0140d --- /dev/null +++ b/genet/validate/network.py @@ -0,0 +1,101 @@ +import networkx as nx +import math +from dataclasses import dataclass, fields + + +def validate_attribute_data(attributes, necessary_attributes): + missing_attribs = set(necessary_attributes) - set(attributes) + if missing_attribs: + raise AttributeError(f'Attributes: {missing_attribs} missing from data: {attributes}') + + +def find_problem_nodes(G): + problem_nodes = {} + problem_nodes['dead_ends'] = [] + problem_nodes['unreachable_node'] = [] + for node in G.nodes: + if (G.in_degree(node) == 0): + problem_nodes['unreachable_node'].append(node) + if (G.out_degree(node) == 0): + problem_nodes['dead_ends'].append(node) + return problem_nodes + + +def find_connected_subgraphs(G): + return [(list(c), len(c)) for c in sorted(nx.strongly_connected_components(G), key=len, reverse=True)] + + +def describe_graph_connectivity(G): + """ + Computes dead ends and unreachable nodes in G. Computes strongly connected components of G + :param G: + :return: + """ + dict_to_return = {} + # find dead ends or unreachable nodes + dict_to_return['problem_nodes'] = find_problem_nodes(G) + # find number of connected subgraphs + dict_to_return['number_of_connected_subgraphs'] = len(find_connected_subgraphs(G)) + return dict_to_return + + +def evaluate_condition_for_floatable(value, condition): + try: + value = float(value) + return condition(value) + except (ValueError, TypeError): + return False + + +def zero_value(value): + return value == 0.0 + + +def negative_value(value): + return value < 0.0 + + +def infinity_value(value): + return math.isinf(value) + + +def fractional_value(value): + return 1.0 > value > 0.0 + + +def none_condition(value): + return value in [None, 'None'] + + +@dataclass() +class Condition: + condition: callable + + def evaluate(self, value): + return self.condition(value) + + +@dataclass() +class FloatCondition(Condition): + condition: callable + + def evaluate(self, value): + return evaluate_condition_for_floatable(value, self.condition) + + +@dataclass() +class ConditionsToolbox: + zero: Condition = FloatCondition(zero_value) + negative: Condition = FloatCondition(negative_value) + infinite: Condition = FloatCondition(infinity_value) + fractional: Condition = FloatCondition(fractional_value) + none: Condition = Condition(none_condition) + + def condition_names(self) -> list: + return [field.name for field in fields(self)] + + def get_condition_evaluator(self, condition: str) -> callable: + if condition in self.__dict__: + return self.__dict__[condition].evaluate + else: + raise NotImplementedError(f'Condition {condition} is not defined.') diff --git a/genet/validate/network_validation.py b/genet/validate/network_validation.py deleted file mode 100644 index 0975411d..00000000 --- a/genet/validate/network_validation.py +++ /dev/null @@ -1,37 +0,0 @@ -import networkx as nx - - -def validate_attribute_data(attributes, necessary_attributes): - missing_attribs = set(necessary_attributes) - set(attributes) - if missing_attribs: - raise AttributeError(f'Attributes: {missing_attribs} missing from data: {attributes}') - - -def find_problem_nodes(G): - problem_nodes = {} - problem_nodes['dead_ends'] = [] - problem_nodes['unreachable_node'] = [] - for node in G.nodes: - if (G.in_degree(node) == 0): - problem_nodes['unreachable_node'].append(node) - if (G.out_degree(node) == 0): - problem_nodes['dead_ends'].append(node) - return problem_nodes - - -def find_connected_subgraphs(G): - return [(list(c), len(c)) for c in sorted(nx.strongly_connected_components(G), key=len, reverse=True)] - - -def describe_graph_connectivity(G): - """ - Computes dead ends and unreachable nodes in G. Computes strongly connected components of G - :param G: - :return: - """ - dict_to_return = {} - # find dead ends or unreachable nodes - dict_to_return['problem_nodes'] = find_problem_nodes(G) - # find number of connected subgraphs - dict_to_return['number_of_connected_subgraphs'] = len(find_connected_subgraphs(G)) - return dict_to_return diff --git a/genet/validate/schedule_validation.py b/genet/validate/schedule.py similarity index 100% rename from genet/validate/schedule_validation.py rename to genet/validate/schedule.py diff --git a/tests/test_core_network.py b/tests/test_core_network.py index ce92a235..fb642eed 100644 --- a/tests/test_core_network.py +++ b/tests/test_core_network.py @@ -18,6 +18,7 @@ from tests.test_output_matsim_xml_writer import network_dtd, schedule_dtd from genet.schedule_elements import Route, Service, Schedule, Stop from genet.utils import plot, spatial +from genet.validate import network as network_validation from genet.input import read from tests.fixtures import assert_semantically_equal, route, stop_epsg_27700, network_object_from_test_data, \ full_fat_default_config_path, correct_schedule, vehicle_definitions_config_path @@ -2643,78 +2644,140 @@ def test_invalid_network_routes_with_empty_route(route): assert n.invalid_network_routes() == ['route'] -def test_generate_validation_report_with_pt2matsim_network(network_object_from_test_data): - n = network_object_from_test_data - report = n.generate_validation_report() - correct_report = { - 'graph': { - 'graph_connectivity': { - 'car': {'problem_nodes': {'dead_ends': ['21667818'], 'unreachable_node': ['25508485']}, - 'number_of_connected_subgraphs': 2}, - 'walk': {'problem_nodes': {'dead_ends': ['21667818'], 'unreachable_node': ['25508485']}, - 'number_of_connected_subgraphs': 2}, - 'bike': {'problem_nodes': {'dead_ends': [], 'unreachable_node': []}, - 'number_of_connected_subgraphs': 0}}, - 'link_attributes': { - 'links_over_1km_length': {'number_of': 0, 'percentage': 0.0, 'link_ids': []}, - 'zero_attributes': {}}}, - 'schedule': { - 'schedule_level': {'is_valid_schedule': False, 'invalid_stages': ['not_has_valid_services'], - 'has_valid_services': False, 'invalid_services': ['10314']}, - 'service_level': { - '10314': {'is_valid_service': False, 'invalid_stages': ['not_has_valid_routes'], - 'has_valid_routes': False, 'invalid_routes': ['VJbd8660f05fe6f744e58a66ae12bd66acbca88b98']}}, - 'route_level': {'10314': {'VJbd8660f05fe6f744e58a66ae12bd66acbca88b98': {'is_valid_route': False, - 'invalid_stages': [ - 'not_has_correctly_ordered_route']}}}, - 'vehicle_level': {'vehicle_definitions_valid': True, - 'vehicle_definitions_validity_components': { - 'missing_vehicles': {'missing_vehicles_types': set(), - 'vehicles_affected': {}}, - 'multiple_use_vehicles': {}, - 'unused_vehicles': set()}}}, - - 'routing': {'services_have_routes_in_the_graph': False, - 'service_routes_with_invalid_network_route': ['VJbd8660f05fe6f744e58a66ae12bd66acbca88b98'], - 'route_to_crow_fly_ratio': { - '10314': {'VJbd8660f05fe6f744e58a66ae12bd66acbca88b98': 'Division by zero'}}}} - assert_semantically_equal(report, correct_report) +@pytest.fixture() +def invalid_pt2matsim_network_for_validation(network_object_from_test_data): + return { + 'network': network_object_from_test_data, + 'subgraph_no_per_mode': { + 'car': 2, + 'walk': 2, + 'bike': 0 + }, + 'is_valid_schedule': False, + 'invalid_service_id': '10314', + 'valid_PT_network_routes': False, + 'pt_routes_with_invalid_network_route': ['VJbd8660f05fe6f744e58a66ae12bd66acbca88b98'], + } + + +def test_connectivity_in_report_with_invalid_network(invalid_pt2matsim_network_for_validation): + report = invalid_pt2matsim_network_for_validation['network'].generate_validation_report() + for mode, expected_connected_subgraphs in invalid_pt2matsim_network_for_validation['subgraph_no_per_mode'].items(): + assert report['graph']['graph_connectivity'][mode]['number_of_connected_subgraphs'] == expected_connected_subgraphs + + +def test_schedule_validity_in_report_with_invalid_network(invalid_pt2matsim_network_for_validation): + report = invalid_pt2matsim_network_for_validation['network'].generate_validation_report() + assert report['schedule']['schedule_level']['is_valid_schedule'] == invalid_pt2matsim_network_for_validation['is_valid_schedule'] + + +def test_invalid_service_identified_in_report_with_invalid_network(invalid_pt2matsim_network_for_validation): + report = invalid_pt2matsim_network_for_validation['network'].generate_validation_report() + invalid_service_id = invalid_pt2matsim_network_for_validation['invalid_service_id'] + assert not report['schedule']['service_level'][invalid_service_id]['is_valid_service'] + + +def test_network_routing_in_report_with_invalid_network(invalid_pt2matsim_network_for_validation): + report = invalid_pt2matsim_network_for_validation['network'].generate_validation_report() + assert report['routing']['services_have_routes_in_the_graph'] == invalid_pt2matsim_network_for_validation['valid_PT_network_routes'] + +def test_invalid_network_routes_show_in_report_with_invalid_network(invalid_pt2matsim_network_for_validation): + report = invalid_pt2matsim_network_for_validation['network'].generate_validation_report() + route_ids_with_invalid_network_route = invalid_pt2matsim_network_for_validation['pt_routes_with_invalid_network_route'] + assert report['routing']['service_routes_with_invalid_network_route'] == route_ids_with_invalid_network_route -def test_generate_validation_report_with_correct_schedule(correct_schedule): + +@pytest.fixture() +def valid_network_for_validation(correct_schedule): n = Network('epsg:27700') n.add_link('1', 1, 2, attribs={'length': 2, "modes": ['car', 'bus']}) - n.add_link('2', 2, 3, attribs={'length': 2, "modes": ['car', 'bus']}) + n.add_link('2', 2, 1, attribs={'length': 2, "modes": ['car', 'bus']}) n.schedule = correct_schedule + return { + 'network': n, + 'subgraph_no_per_mode': { + 'car': 1, + 'walk': 0, + 'bike': 0 + }, + 'is_valid_schedule': True, + 'has_valid_PT_network_routes': True + } + + +def test_connectivity_in_report_with_valid_network(valid_network_for_validation): + report = valid_network_for_validation['network'].generate_validation_report() + for mode, expected_connected_subgraphs in valid_network_for_validation['subgraph_no_per_mode'].items(): + assert report['graph']['graph_connectivity'][mode]['number_of_connected_subgraphs'] == expected_connected_subgraphs + + +def test_schedule_validity_in_report_with_valid_network(valid_network_for_validation): + report = valid_network_for_validation['network'].generate_validation_report() + assert report['schedule']['schedule_level']['is_valid_schedule'] == valid_network_for_validation['is_valid_schedule'] + + +def test_network_routing_in_report_with_valid_network(valid_network_for_validation): + report = valid_network_for_validation['network'].generate_validation_report() + assert report['routing']['services_have_routes_in_the_graph'] == valid_network_for_validation['has_valid_PT_network_routes'] + + +def test_long_links_show_up_in_validation_report(): + n = Network('epsg:27700') + n.add_link('1', 1, 2, attribs={'length': 10000, 'capacity': 1, 'freespeed': 1, "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + report = n.generate_validation_report() - correct_report = { - 'graph': { - 'graph_connectivity': {'car': {'problem_nodes': {'dead_ends': [3], 'unreachable_node': [1]}, - 'number_of_connected_subgraphs': 3}, - 'walk': {'problem_nodes': {'dead_ends': [], 'unreachable_node': []}, - 'number_of_connected_subgraphs': 0}, - 'bike': {'problem_nodes': {'dead_ends': [], 'unreachable_node': []}, - 'number_of_connected_subgraphs': 0}}, - 'link_attributes': {'links_over_1km_length': {'number_of': 0, 'percentage': 0.0, 'link_ids': []}, - 'zero_attributes': {}}}, - 'schedule': {'schedule_level': {'is_valid_schedule': True, 'invalid_stages': [], 'has_valid_services': True, - 'invalid_services': []}, - 'service_level': { - 'service': {'is_valid_service': True, 'invalid_stages': [], 'has_valid_routes': True, - 'invalid_routes': []}}, - 'route_level': { - 'service': {'1': {'is_valid_route': True, 'invalid_stages': []}, - '2': {'is_valid_route': True, 'invalid_stages': []}}}, - 'vehicle_level': {'vehicle_definitions_valid': True, - 'vehicle_definitions_validity_components': { - 'missing_vehicles': {'missing_vehicles_types': set(), - 'vehicles_affected': {}}, - 'multiple_use_vehicles': {}, - 'unused_vehicles': set()}}}, - 'routing': {'services_have_routes_in_the_graph': True, 'service_routes_with_invalid_network_route': [], - 'route_to_crow_fly_ratio': {'service': {'1': 0.037918141839160244, '2': 0.037918141839160244}}}} - assert_semantically_equal(report, correct_report) + + assert_semantically_equal( + report['graph']['link_attributes']['links_over_1000_length'], + {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']} + ) + + +offending_link_attribute_values_and_names = [('zero', '0'), ('negative', '-1'), ('infinite', 'inf'), ('fractional', '0.1'), ('none', 'None')] + +@pytest.mark.parametrize("value,offending_value", offending_link_attribute_values_and_names) +def test_values_of_ids_are_not_flagged_in_validation_report(value, offending_value): + n = Network('epsg:27700') + n.add_link(offending_value, 1, 2, attribs={'length': 1, 'capacity': 1, 'freespeed': 1, "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes'][f'{value}_attributes'], + {} + ) + + +@pytest.mark.parametrize("value,offending_value", offending_link_attribute_values_and_names) +def test_values_of_from_node_are_not_flagged_in_validation_report(value, offending_value): + n = Network('epsg:27700') + n.add_link('1', offending_value, 2, attribs={'length': 1, 'capacity': 1, 'freespeed': 1, "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes'][f'{value}_attributes'], + {} + ) + + +@pytest.mark.parametrize("value,offending_value", offending_link_attribute_values_and_names) +def test_values_of_to_node_are_not_flagged_in_validation_report(value, offending_value): + n = Network('epsg:27700') + n.add_link('1', 1, offending_value, attribs={'length': 1, 'capacity': 1, 'freespeed': 1, "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes'][f'{value}_attributes'], + {} + ) def test_zero_value_attributes_show_up_in_validation_report(): @@ -2723,18 +2786,107 @@ def test_zero_value_attributes_show_up_in_validation_report(): n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) report = n.generate_validation_report() - correct_report = {'graph': { - 'graph_connectivity': { - 'car': {'problem_nodes': {'dead_ends': [3], 'unreachable_node': [1]}, 'number_of_connected_subgraphs': 3}, - 'walk': {'problem_nodes': {'dead_ends': [], 'unreachable_node': []}, 'number_of_connected_subgraphs': 0}, - 'bike': {'problem_nodes': {'dead_ends': [], 'unreachable_node': []}, 'number_of_connected_subgraphs': 0}}, - 'link_attributes': { - 'links_over_1km_length': {'number_of': 0, 'percentage': 0.0, 'link_ids': []}, - 'zero_attributes': { - 'length': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, - 'capacity': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, - 'freespeed': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}}}}} - assert_semantically_equal(report, correct_report) + + assert_semantically_equal( + report['graph']['link_attributes']['zero_attributes'], + { + 'length': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, + 'capacity': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, + 'freespeed': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']} + } + ) + + +def test_negative_value_attributes_show_up_in_validation_report(): + n = Network('epsg:27700') + n.add_link('1', 1, 2, attribs={'length': -1, 'capacity': 1, 'freespeed': '-5', "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes']['negative_attributes'], + { + 'length': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, + 'freespeed': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']} + } + ) + + +def test_infinite_value_attributes_show_up_in_validation_report(): + n = Network('epsg:27700') + n.add_link('1', 1, 2, attribs={'length': float('inf'), 'capacity': 0.0, 'freespeed': 'inf', "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes']['infinite_attributes'], + { + 'length': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, + 'freespeed': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']} + } + ) + + +def test_fractional_value_attributes_show_up_in_validation_report(): + n = Network('epsg:27700') + n.add_link('1', 1, 2, attribs={'length': 0.1, 'capacity': '0.0', 'freespeed': '0.2', "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes']['fractional_attributes'], + { + 'length': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, + 'freespeed': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']} + } + ) + + +def test_none_value_attributes_show_up_in_validation_report(): + n = Network('epsg:27700') + n.add_link('1', 1, 2, attribs={'length': 1, 'capacity': 'None', 'freespeed': None, "modes": ['car', 'bus']}) + n.add_link('2', 2, 3, attribs={'length': 2, 'capacity': 1, 'freespeed': 2, "modes": ['car', 'bus']}) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes']['none_attributes'], + { + 'capacity': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']}, + 'freespeed': {'number_of': 1, 'percentage': 0.5, 'link_ids': ['1']} + } + ) + + +def test_nested_values_show_up_in_validation_report(): + n = Network('epsg:27700') + n.add_link('1', 1, 2, + attribs={'length': 1, 'capacity': '0.0', 'freespeed': '2', "modes": ['car', 'bus'], + 'attributes': {'osm:way:lanes': -1} + }) + + report = n.generate_validation_report() + + assert_semantically_equal( + report['graph']['link_attributes']['negative_attributes'], + { + 'attributes::osm:way:lanes': {'number_of': 1, 'percentage': 1, 'link_ids': ['1']}, + } + ) + + +def test_check_connectivity_for_mode_warns_of_graphs_with_more_than_single_component(mocker, caplog): + mocker.patch.object(network_validation, 'describe_graph_connectivity', + return_value={'problem_nodes': {'dead_ends': [], 'unreachable_node': ['1']}, + 'number_of_connected_subgraphs': 2}) + + Network('epsg:27700').check_connectivity_for_mode('car') + + assert caplog.records[0].levelname == 'WARNING' + assert 'more than one connected component' in caplog.records[0].message def test_write_to_matsim_generates_three_matsim_files(network_object_from_test_data, tmpdir): diff --git a/tests/test_core_schedule.py b/tests/test_core_schedule.py index fea0a9a3..3faa6d9a 100644 --- a/tests/test_core_schedule.py +++ b/tests/test_core_schedule.py @@ -6,7 +6,7 @@ from genet.input import read, matsim_reader, gtfs_reader from genet.schedule_elements import Schedule, Service, Route, Stop, read_vehicle_types from genet.utils import plot, spatial -from genet.validate import schedule_validation +from genet.validate import schedule as schedule_validation from tests.fixtures import * from tests.test_core_components_service import service from tests.test_core_components_route import self_looping_route, route diff --git a/tests/test_utils_dict_support.py b/tests/test_utils_dict_support.py index 5ab23735..e4679454 100644 --- a/tests/test_utils_dict_support.py +++ b/tests/test_utils_dict_support.py @@ -131,6 +131,14 @@ def test_merging_dicts_with_lists_when_one_dict_is_empty(): assert_semantically_equal(d, {'1': [''], '2': []}) +def test_simple_dict_to_string(): + assert dict_support.dict_to_string({'simple': 'nest'}) == 'simple::nest' + + +def test_deeper_dict_to_string(): + assert dict_support.dict_to_string({'deeper': {'nested': 'dict'}}) == 'deeper::nested::dict' + + def test_dataframe_to_dict_returns_dictionary_ignoring_nan_values(): df = DataFrame( { diff --git a/tests/test_utils_graph_operations.py b/tests/test_utils_graph_operations.py index 29c2febb..171aedce 100644 --- a/tests/test_utils_graph_operations.py +++ b/tests/test_utils_graph_operations.py @@ -341,6 +341,24 @@ def test_get_attribute_schema_merges_lists(): assert output_tree == [(0, None, 'attribute'), (1, 'attribute', 'modes', {'bike', 'car', 'walk'})] +def test_parsing_flat_attribute_from_attributes_tree(): + links = [('1', {'length': 1})] + node = list(graph_operations.get_attribute_schema(links).leaves)[0] + assert graph_operations.parse_leaf(node) == 'length' + + +def test_parsing_nested_attribute_from_attributes_tree(): + links = [('1', {'attributes': {'hello': 1}})] + node = list(graph_operations.get_attribute_schema(links).leaves)[0] + assert graph_operations.parse_leaf(node) == {'attributes': 'hello'} + + +def test_parsing_extra_nested_attribute_from_attributes_tree(): + links = [('1', {'attributes': {'hello': {'text': {'moar': 1}}}})] + node = list(graph_operations.get_attribute_schema(links).leaves)[0] + assert graph_operations.parse_leaf(node) == {'attributes': {'hello': {'text': 'moar'}}} + + def test_get_attribute_data_under_key_with_non_nested_key(): input_list = [ ('0', {'attributes': {'osm:way:highway': {'name': 'osm:way:highway', diff --git a/tests/test_utils_schedule_operations.py b/tests/test_utils_schedule_operations.py index 5bc179de..59459d5b 100644 --- a/tests/test_utils_schedule_operations.py +++ b/tests/test_utils_schedule_operations.py @@ -1,7 +1,7 @@ import pytest from genet import Schedule, Service, Route, Stop, schedule_elements -from genet.validate import schedule_validation +from genet.validate import schedule as schedule_validation from tests.fixtures import test_schedule, assert_semantically_equal @pytest.fixture() diff --git a/tests/test_validate_network.py b/tests/test_validate_network.py new file mode 100644 index 00000000..79c816d3 --- /dev/null +++ b/tests/test_validate_network.py @@ -0,0 +1,25 @@ +import pytest +from genet.validate.network import * + + +@pytest.mark.parametrize( + "value,condition,expected_result", + [ + ('0.00', zero_value, True), ('0.0', zero_value, True), ('zero', zero_value, False), + ('-1', negative_value, True), ('non-negative', negative_value, False), + ('-inf', infinity_value, True), ('inf', infinity_value, True), ('infinity', infinity_value, True), + ('0.01', fractional_value, True), ('0.00', fractional_value, False), + ] +) +def test_string_cases_for_float_conditions(value, condition, expected_result): + assert FloatCondition(condition).evaluate(value) == expected_result + + +@pytest.mark.parametrize( + "value,condition,expected_result", + [ + ('None', none_condition, True), (None, none_condition, True) + ] +) +def test_cases_for_none_conditions(value, condition, expected_result): + assert Condition(condition).evaluate(value) == expected_result \ No newline at end of file