From 5593e129a11bed7e1858966674c2c1eefa284173 Mon Sep 17 00:00:00 2001 From: karthikkadajji Date: Fri, 28 Aug 2020 23:41:35 +0200 Subject: [PATCH 1/2] added logging functionality --- log/logfile.log | 22 ++++++++++++++++++++++ log/logfile.old.20200828-233531.log | 22 ++++++++++++++++++++++ log/logfile.old.20200828-233614.log | 22 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 log/logfile.log create mode 100644 log/logfile.old.20200828-233531.log create mode 100644 log/logfile.old.20200828-233614.log diff --git a/log/logfile.log b/log/logfile.log new file mode 100644 index 0000000..c7542ad --- /dev/null +++ b/log/logfile.log @@ -0,0 +1,22 @@ +2020-08-28 23:36:14,693, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Number of geojson files read : 2 +2020-08-28 23:36:14,693, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call,Processing File : ms_campus_nodes.geojson ms_campus_ts.geojson +2020-08-28 23:36:22,698, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Running intersectingvalidation to see if any ways which are intersecting have a missing intersecting node +2020-08-28 23:36:45,509, IntersectingValidation, INFO , function:intersectLineStringInValidFormat, step1, 0 calls, 0 seconds, 0.000 seconds per call,half way through... please wait +2020-08-28 23:36:54,073, main, INFO , function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing Exploratory Data Analysis +2020-08-28 23:36:54,262, EDA, INFO , function: subgraph_eda, step 3, 1 calls, 0.00872 seconds, 0.00872 seconds per call, Number of ways in the file : 4189 Number of isolated ways: 17 Number of Connected ways: 4172 Number of Connected Components : 52 +2020-08-28 23:36:54,515, main, INFO , function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing all validations +2020-08-28 23:36:54,518, utildata, INFO , function: get_one_node_ways, step1, 1 calls, 0.000 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:36:54,518, utildata, INFO , function: get_coord_df, step2, 1 calls, 7.810 seconds, 7.810 seconds per call, No Comments +2020-08-28 23:36:54,518, utildata, INFO , function: split_ways_geojson_file, step3, 1 calls, 0.140 seconds, 0.140 seconds per call, No Comments +2020-08-28 23:36:54,518, utildata, INFO , function: get_isolated_ways, step4, 1 calls, 0.006 seconds, 0.006 seconds per call, No Comments +2020-08-28 23:36:54,518, utildata, INFO , function: get_coord_dict, step5, 1 calls, 0.031 seconds, 0.031 seconds per call, No Comments +2020-08-28 23:36:54,518, utildata, INFO , function: get_coords_list, step6, 2 calls, 0.001 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:36:54,518, EDA, INFO , function: get_invalidNodes, step1, 1 calls, 0.003 seconds, 0.003 seconds per call, No Comments +2020-08-28 23:36:54,518, EDA, INFO , function: get_way_from_subgraph, step2, 1 calls, 0.006 seconds, 0.006 seconds per call, No Comments +2020-08-28 23:36:54,518, EDA, INFO , function: subgraph_eda, step3, 1 calls, 0.266 seconds, 0.266 seconds per call, No Comments +2020-08-28 23:36:54,519, EDA, INFO , function: plot_nodes_vs_ways, step4, 1 calls, 0.176 seconds, 0.176 seconds per call, No Comments +2020-08-28 23:36:54,519, IntersectingValidation, INFO , function: intersectLineStringInValidFormat, step1, 1 calls, 31.374 seconds, 31.374 seconds per call, No Comments +2020-08-28 23:36:54,519, IntersectingValidation, INFO , function: geojsonWrite, step2, 3 calls, 0.003 seconds, 0.001 seconds per call, No Comments +2020-08-28 23:36:54,519, IntersectingValidation, INFO , function: brunnelcheck, step3, 4189 calls, 0.010 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:36:54,519, IntersectingValidation, INFO , function: indexInvalidGeometryType, step4, 1 calls, 0.060 seconds, 0.060 seconds per call, No Comments +2020-08-28 23:36:54,519, IntersectingValidation, INFO , function: geometryFormat, step5, 8378 calls, 0.071 seconds, 0.000 seconds per call, No Comments diff --git a/log/logfile.old.20200828-233531.log b/log/logfile.old.20200828-233531.log new file mode 100644 index 0000000..7524a1b --- /dev/null +++ b/log/logfile.old.20200828-233531.log @@ -0,0 +1,22 @@ +2020-08-28 23:34:26,176, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Number of geojson files read : 2 +2020-08-28 23:34:26,176, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call,Processing File : ms_campus_nodes.geojson ms_campus_ts.geojson +2020-08-28 23:34:34,075, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Running intersectingvalidation to see if any ways which are intersecting have a missing intersecting node +2020-08-28 23:34:58,265, IntersectingValidation, INFO , function:intersectLineStringInValidFormat, step1, 0 calls, 0 seconds, 0.000 seconds per call,half way through... please wait +2020-08-28 23:35:08,069, main, INFO , function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing Exploratory Data Analysis +2020-08-28 23:35:08,256, EDA, INFO , function: subgraph_eda, step 3, 1 calls, 0.00878 seconds, 0.00878 seconds per call, Number of ways in the file : 4189 Number of isolated ways: 17 Number of Connected ways: 4172 Number of Connected Components : 52 +2020-08-28 23:35:08,518, main, INFO , function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing all validations +2020-08-28 23:35:08,522, utildata, INFO , function: get_one_node_ways, step1, 1 calls, 0.000 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:35:08,522, utildata, INFO , function: get_coord_df, step2, 1 calls, 7.702 seconds, 7.702 seconds per call, No Comments +2020-08-28 23:35:08,522, utildata, INFO , function: split_ways_geojson_file, step3, 1 calls, 0.142 seconds, 0.142 seconds per call, No Comments +2020-08-28 23:35:08,522, utildata, INFO , function: get_isolated_ways, step4, 1 calls, 0.006 seconds, 0.006 seconds per call, No Comments +2020-08-28 23:35:08,522, utildata, INFO , function: get_coord_dict, step5, 1 calls, 0.031 seconds, 0.031 seconds per call, No Comments +2020-08-28 23:35:08,522, utildata, INFO , function: get_coords_list, step6, 2 calls, 0.001 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:35:08,522, EDA, INFO , function: get_invalidNodes, step1, 1 calls, 0.003 seconds, 0.003 seconds per call, No Comments +2020-08-28 23:35:08,522, EDA, INFO , function: get_way_from_subgraph, step2, 1 calls, 0.007 seconds, 0.007 seconds per call, No Comments +2020-08-28 23:35:08,522, EDA, INFO , function: subgraph_eda, step3, 1 calls, 0.275 seconds, 0.275 seconds per call, No Comments +2020-08-28 23:35:08,522, EDA, INFO , function: plot_nodes_vs_ways, step4, 1 calls, 0.174 seconds, 0.174 seconds per call, No Comments +2020-08-28 23:35:08,522, IntersectingValidation, INFO , function: intersectLineStringInValidFormat, step1, 1 calls, 33.994 seconds, 33.994 seconds per call, No Comments +2020-08-28 23:35:08,522, IntersectingValidation, INFO , function: geojsonWrite, step2, 3 calls, 0.003 seconds, 0.001 seconds per call, No Comments +2020-08-28 23:35:08,522, IntersectingValidation, INFO , function: brunnelcheck, step3, 4189 calls, 0.010 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:35:08,522, IntersectingValidation, INFO , function: indexInvalidGeometryType, step4, 1 calls, 0.058 seconds, 0.058 seconds per call, No Comments +2020-08-28 23:35:08,522, IntersectingValidation, INFO , function: geometryFormat, step5, 8378 calls, 0.069 seconds, 0.000 seconds per call, No Comments diff --git a/log/logfile.old.20200828-233614.log b/log/logfile.old.20200828-233614.log new file mode 100644 index 0000000..8251b7c --- /dev/null +++ b/log/logfile.old.20200828-233614.log @@ -0,0 +1,22 @@ +2020-08-28 23:35:31,145, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Number of geojson files read : 2 +2020-08-28 23:35:31,145, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call,Processing File : ms_campus_nodes.geojson ms_campus_ts.geojson +2020-08-28 23:35:38,864, main, INFO , function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Running intersectingvalidation to see if any ways which are intersecting have a missing intersecting node +2020-08-28 23:36:01,357, IntersectingValidation, INFO , function:intersectLineStringInValidFormat, step1, 0 calls, 0 seconds, 0.000 seconds per call,half way through... please wait +2020-08-28 23:36:10,648, main, INFO , function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing Exploratory Data Analysis +2020-08-28 23:36:10,836, EDA, INFO , function: subgraph_eda, step 3, 1 calls, 0.00868 seconds, 0.00868 seconds per call, Number of ways in the file : 4189 Number of isolated ways: 17 Number of Connected ways: 4172 Number of Connected Components : 52 +2020-08-28 23:36:11,093, main, INFO , function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing all validations +2020-08-28 23:36:11,096, utildata, INFO , function: get_one_node_ways, step1, 1 calls, 0.000 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:36:11,096, utildata, INFO , function: get_coord_df, step2, 1 calls, 7.520 seconds, 7.520 seconds per call, No Comments +2020-08-28 23:36:11,096, utildata, INFO , function: split_ways_geojson_file, step3, 1 calls, 0.143 seconds, 0.143 seconds per call, No Comments +2020-08-28 23:36:11,096, utildata, INFO , function: get_isolated_ways, step4, 1 calls, 0.006 seconds, 0.006 seconds per call, No Comments +2020-08-28 23:36:11,096, utildata, INFO , function: get_coord_dict, step5, 1 calls, 0.031 seconds, 0.031 seconds per call, No Comments +2020-08-28 23:36:11,097, utildata, INFO , function: get_coords_list, step6, 2 calls, 0.001 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:36:11,097, EDA, INFO , function: get_invalidNodes, step1, 1 calls, 0.003 seconds, 0.003 seconds per call, No Comments +2020-08-28 23:36:11,097, EDA, INFO , function: get_way_from_subgraph, step2, 1 calls, 0.008 seconds, 0.008 seconds per call, No Comments +2020-08-28 23:36:11,097, EDA, INFO , function: subgraph_eda, step3, 1 calls, 0.269 seconds, 0.269 seconds per call, No Comments +2020-08-28 23:36:11,097, EDA, INFO , function: plot_nodes_vs_ways, step4, 1 calls, 0.175 seconds, 0.175 seconds per call, No Comments +2020-08-28 23:36:11,097, IntersectingValidation, INFO , function: intersectLineStringInValidFormat, step1, 1 calls, 31.784 seconds, 31.784 seconds per call, No Comments +2020-08-28 23:36:11,097, IntersectingValidation, INFO , function: geojsonWrite, step2, 3 calls, 0.003 seconds, 0.001 seconds per call, No Comments +2020-08-28 23:36:11,097, IntersectingValidation, INFO , function: brunnelcheck, step3, 4189 calls, 0.010 seconds, 0.000 seconds per call, No Comments +2020-08-28 23:36:11,097, IntersectingValidation, INFO , function: indexInvalidGeometryType, step4, 1 calls, 0.060 seconds, 0.060 seconds per call, No Comments +2020-08-28 23:36:11,097, IntersectingValidation, INFO , function: geometryFormat, step5, 8378 calls, 0.073 seconds, 0.000 seconds per call, No Comments From 3608bdb1db8ba2d0d36af8a1427109d7c9830144 Mon Sep 17 00:00:00 2001 From: karthikkadajji Date: Fri, 28 Aug 2020 23:42:36 +0200 Subject: [PATCH 2/2] Added logging facility --- TestData/Output/NodesVsWays.PNG | Bin 20045 -> 20088 bytes TestData/Output/SampleSubgraph.PNG | Bin 22207 -> 21871 bytes intersectingValidation.py | 22 +- logging_config.ini | 85 +++ main.py | 36 +- node_connectivity.py | 27 +- requirements.txt | 3 +- timerLog.py | 864 +++++++++++++++++++++++++++++ util_data.py | 21 +- 9 files changed, 1036 insertions(+), 22 deletions(-) create mode 100644 logging_config.ini create mode 100644 timerLog.py diff --git a/TestData/Output/NodesVsWays.PNG b/TestData/Output/NodesVsWays.PNG index 597d5cd385f26aca0aeffe0c10a3c03145d86737..9151a401542fe71f7931e0672d4eef31367122cd 100644 GIT binary patch literal 20088 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�uOwF;WHmX_8IPR>=(vxGisa?%6(IC*)m12c~v zeHA&$LiyQVuoX)QGva;oCJoU&1WFPU=}@22($lY^dO`B2yU2RTx~&vl2y_6>PhY;A zLzii4ZZ7cA#@zfh;=mLZ78d+?myi(s+O=z^s!yI!+)AcJ>5&`0`IN9d&|@L4l+a-QesGvlD~;e*y~w^CC01173Mgz5D3^h9jGQt;km$BylX zy`)>E4tvHXtk}Nyei}wA!ejEu zhGVx-ZK#esdi01fIL~7)8&J!D@bR%#JA?Sn?m?QuBziAqWN zC{mJ_r%l-aMT+6rs!K@Q8ev?mI||LxpF$qGgLKuZo!8S|zT61EbS`yaZ&d8o8p@_R zZyfYEhlv8gVEZMC_=};*({S_>Fd9I`EqZe=EH~ASkxO?Snh(;1uPspF8Aumsd2F!M0*0Cz|(0a-P)I%(vKNn6-tw7f1H;xY)4!7lD-qvS@)Yv)cXSf_k2Z*C&2!v_8QD1c;fc5oj?vkS6R8Q$@%&Q!_3 zV7-As=h2DVx1RE%OE6W!pEsJ*jRK=CM;TSt*S}v{dZe+bseOtieSKndG&SOitJbV} zgXNzFyq;UPz3b@XMxFEUoskJq<&1QNBj%VG1U5!(gyLarM0GHn&=y1Tg zE%)fuvyz1^;KGPT7(z8ht*ro7k@^4x>Jq@8B|yt?^z`X9_%l|*g;-#=c6I^;)Ta?P z$DgzCh3xjavXTxax`@}JILZs)H$}hb4vsd^!NWLp`t%!=kl(P?*5xan82_Pk%d0c) zIC`NIio#E+{-c`Wdx0|Sga2El_`i5_)e@Eenw@kBM@(#?)=*?mDx63;W_a~~0ep>l A*8l(j literal 20045 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 Neo^{D{5h?^{}1nI4I}^n literal 22207 zcmeIac{rEd`!@O^N>rvOA(AE`iA3hiR7glNCPSGeLxz;8LMo++Xh7y9BtwKE35g;@ zGAA-*W}oZR^V{!x?0p>j&;8fi@jS=(`F6YSd);fT>$=YCJkKRu_mKK(#tn=V3T3sX zhKfFgLK{e-&~!2^$KOcSxBbNb(77vX8Zh9`d4`i=`2Q*AbD9o1RUz%ivBzyej zJ`Yu6k0Z{fJkD9V*-#uUJzUN>dz`Vi;`g+1bGLVP+9N71DlW`#=i%WZvuoG?x-RPM zX1nVa-_>vmg`c9SqGaH8|JQeK(}OeJ%SZd#4Py*dH`T1OexIKg@mN_oRxc#`#+DGS zR^#F?>n~<|*k^Yssyex+c~`s~Q}z6GC2p9pvP zsr!9#zw@JVwCI#r6jktN!vP^riU9ry9TN_u{zVA=wXm=-jq)rC8!IcT{d!JCa(Q(* ztp+YXwlt=M&c0yq?9N=7F-76c#TCrDMLuV(#@eqSSL8n%+ikV&p76 zteb76O374EtgNgQNZKSIkXYvR=Lh{Wqs)xOjm;{;f$^QSmgd&h)@1P$#no;c?y#Z} z4%XjuGymST%Q{aOUwza6{6g$f!p4B3UFgip=e8?V&S(Ybd2{(OCCU^jcCnD>bJba(F_ zJIyWc411P_y!LdYAKLQakK@(B-sODU53nt*IVz^k9ZR~?EPtD=HSx!%Q+oS8wjZJd z($;jej@PvxQoN_y)xybP^otAIGbGTQ=>@Vtad8`;8KyIun|IKfkN@54ZKS5QoKx7CvVZ^n^&2)My7g3#{{36| zHOJQd>XoRd(9FzT4$T>>4;(lU8yl-AdfWQza~oZu80ku<@2@z1|Nfn9S|C0#F=1hC zeQ{Uuv_P|XN6Sk`?vvtN6O)sc_{;i7>YKN0*-{mIdzZOY*`<|stuIA{MMO45YlvA_ zUX{Jw*WLZ7J;S@m7At-IEv+$KZ|%L?BeKED7fMSN4Khr*C}df5i`_HgU)pmR3~4*L5jNRudaQ^)H(YcWl>zT10ui1$mva+%+ zU1ju>`JX=eI@~w5BIcWLs4*^2qtDWm+ft zw!12->-X=-(a}*JYbodUt8sF^W$)jw3|+%pHZjo1%ENP2^u(*5CcSqhPhX3Urs3m@ zr_F1epBcxPrQh|8(N-a)#n742&#!DZRXu585%~74O8>w>Pi&@vP}k3&bmz{UGkxW; zl6j+?($kS39&2m_hNjeYSzNDw+!*KK>yDH$Tne}n`}5~X35U-N)>V-e*SBa}*xH6J zTd`(s{wgIF-K0ajCv9!J{|>j(%Wu|Wp=)VvT}JsBuMj`9(AV?baqQREJ*LIykI5-) z-n=X%B&7dq&bGad-yX*;j&>brX=$dhzDXo3wkjY};1e z(7-0=Gf`R@!A(Q?`t@tFS&_^JDW|KO)gzOuoW4G%FY=wLL`vuy9!~dPDjnhum-qd< z>ej7WvGRUC#lAE&4{d606}0cc1PGhae~=4Q6%Y|w9UdND@U-s1{&o0|SUKOl6#jvu zoG%_%xHr3sq|lW-sPh)>#FAP%Iz~iqR_FAQwzRa&_nX_heAW8V_7x}N<^47=2Gc%L zj}kSQS?1<^qtWn17g{v*xaTiVAKPGDyDGC+$1*CH>-)CtGCB{WeWLd(7CFLiv_^`nL5eovjyh zb0w{;tSD=<4&KyCHV_irvPJ3eVP*~?gVNm7%`7Y|fivTM)@}K&gEl_CzK<-+L#DB@ zSvfgxS^FJ*W}p-!bd*AIY<(Gg=MLA_L-)fjEL+hlbR}YgBrRp%{{7`uRV&;Icpj-m zls(bD?e;ne0bpvhW82oPy#=-akjZIX(t)bKkm$T72RYOtHqh_cvu83&K!)wu%0DC; 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 CkT?eb diff --git a/intersectingValidation.py b/intersectingValidation.py index 3f32aaf..53ada0d 100644 --- a/intersectingValidation.py +++ b/intersectingValidation.py @@ -1,8 +1,13 @@ import json +import logging +from logging.config import fileConfig + import pandas as pd from shapely.geometry import LineString, Point, Polygon, MultiPoint import time +from timerLog import timecall + def readJsonFile(path): ''' To read Jsonfile with the given path @@ -22,7 +27,7 @@ def readJsonFile(path): dataDict = json.load(data_json) return dataDict - +@timecall(log_name='IntersectingValidation',log_level=logging.INFO,immediate=False,messages="step5") def geometryFormat(geometryDatarow): if (geometryDatarow[0]["type"] == "LineString"): try: @@ -41,13 +46,14 @@ def geometryFormat(geometryDatarow): return "invalid" +@timecall(log_name='IntersectingValidation',log_level=logging.INFO,immediate=False,messages="step4") def indexInvalidGeometryType(geometryData): dataInvalidGeoJsonFormat = geometryData.apply(geometryFormat, axis=1) ind_drop = dataInvalidGeoJsonFormat[dataInvalidGeoJsonFormat.apply(lambda row: row == 'invalid')].index return ind_drop - +@timecall(log_name='IntersectingValidation',log_level=logging.INFO,immediate=False,messages="step3") def brunnelcheck(x, skipTag): if (skipTag in x[0].keys()): if (x[0]["brunnel"] == None): @@ -57,6 +63,7 @@ def brunnelcheck(x, skipTag): else: return False +@timecall(log_name='IntersectingValidation',log_level=logging.INFO,immediate=False,messages="step2") def geojsonWrite(path, invalidWays, originalGeoJsonFile): ''' To write the given invalid ways in the GeoJson format @@ -80,8 +87,11 @@ def geojsonWrite(path, invalidWays, originalGeoJsonFile): with open(path.split('.')[0] + '.geojson', 'w') as fp: json.dump(invalidPaths, fp, indent=4) - +@timecall(log_name='IntersectingValidation',log_level=logging.INFO,immediate=False,messages="step1") def intersectLineStringInValidFormat(geoJSONdata, skipTag, cf): + fileConfig('logging_config.ini') + IntersectingValidationLogger = logging.getLogger("IntersectingValidation") + featuresData = pd.DataFrame(geoJSONdata["features"]) geometryData = pd.DataFrame(featuresData["geometry"]) propertyData = pd.DataFrame(featuresData["properties"]) @@ -93,7 +103,6 @@ def intersectLineStringInValidFormat(geoJSONdata, skipTag, cf): start_time = time.time() invalidGeometryIndex = indexInvalidGeometryType(geometryData).values.tolist() - print("--- %s seconds ---" % (time.time() - start_time)) for counter in range(len(geoJSONdata["features"])): if counter in invalidGeometryIndex: @@ -104,7 +113,8 @@ def intersectLineStringInValidFormat(geoJSONdata, skipTag, cf): geometryDataFormatCopy = geometryDataFormat.copy() for rowIdI, wayI in geometryDataFormat.iteritems(): - if (rowIdI == len(geometryDataFormat) / 2): + if (rowIdI == int(len(geometryDataFormat) / 2)): + IntersectingValidationLogger.info("function:intersectLineStringInValidFormat, step1, 0 calls, 0 seconds, 0.000 seconds per call,half way through... please wait") print("half way through... please wait") if (wayI == "invalid" or rowIdI in brunnelExist): @@ -139,7 +149,7 @@ def intersectLineStringInValidFormat(geoJSONdata, skipTag, cf): {"type": "Feature", "geometry": {"type": "Point", "coordinates": appendPoints[0]}}) else: - print("Invalid format Support not given yet") + IntersectingValidationLogger.error("Invalid format Support not given yet") exit(0) if (len(violatingWayFeatures) == 0): diff --git a/logging_config.ini b/logging_config.ini new file mode 100644 index 0000000..a0dc6b8 --- /dev/null +++ b/logging_config.ini @@ -0,0 +1,85 @@ +[loggers] +keys=root,sLogger,EDA,main,format,read,write,plot,IntersectingValidation,utildata + +[handlers] +keys=consoleHandler,fileHandler + +[formatters] +keys=fileFormatter,consoleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[logger_sLogger] +level=DEBUG +handlers=consoleHandler,fileHandler +qualname=sLogger +propagate=0 + +[logger_EDA] +level=INFO +handlers=consoleHandler,fileHandler +qualname=EDA +propagate=0 + +[logger_utildata] +level=INFO +handlers=consoleHandler,fileHandler +qualname=utildata +propagate=0 + +[logger_IntersectingValidation] +level=INFO +handlers=consoleHandler,fileHandler +qualname=IntersectingValidation +propagate=0 + +[logger_main] +level=INFO +handlers=consoleHandler,fileHandler +qualname=main +propagate=0 + +[logger_format] +level=INFO +handlers=consoleHandler,fileHandler +qualname=format +propagate=0 + +[logger_read] +level=INFO +handlers=consoleHandler,fileHandler +qualname=read +propagate=0 + +[logger_write] +level=INFO +handlers=consoleHandler,fileHandler +qualname=write +propagate=0 + +[logger_plot] +level=INFO +handlers=consoleHandler,fileHandler +qualname=write +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=WARNING +formatter=consoleFormatter +args=(sys.stdout,) + +[handler_fileHandler] +class=handlers.TimedRotatingFileHandler +filemode='w+' +level=DEBUG +formatter=fileFormatter +args=('log/logfile.log',) + +[formatter_fileFormatter] +format=%(asctime)s, %(name)s, %(levelname)-8s, %(message)s + +[formatter_consoleFormatter] +format=%(levelname)s %(message)s diff --git a/main.py b/main.py index c4d6c6f..72a2815 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,24 @@ import argparse as ag import os +from datetime import datetime + from intersectingValidation import intersectLineStringInValidFormat from glob import glob from node_connectivity import plot_nodes_vs_ways, subgraph_eda, get_invalidNodes from config import DefaultConfigs from util_data import UtilData import ntpath +import logging +from logging.config import fileConfig +import shutil if __name__ == '__main__': + filename1 = datetime.now().strftime("%Y%m%d-%H%M%S") + shutil.copy2('log/logfile.log', 'log/logfile.old.' + filename1 + '.log') + open('log/logfile.log', "w+").truncate(0) + + fileConfig('logging_config.ini') + mainLogger = logging.getLogger("main") parser = ag.ArgumentParser() parser.add_argument("--inputPath", help="Relative input path to GeoJSON files", default=os.path.join(os.getcwd(), "OSW\TestData\input")) @@ -21,30 +32,43 @@ json_files = glob(os.path.join(inputPath, "*.geojson")) if cf.file_filter: - print("Filtering") json_files = sorted([i for i in json_files if cf.file_filter in i]) - print("Number of geojson files :", len(json_files)) + print("Number of geojson files read :", len(json_files)) + mainLogger.info( + "function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Number of geojson files read : %s", + len(json_files)) nodes_files = sorted([x for x in json_files if 'node' in x]) ways_files = sorted([x for x in json_files if 'node' not in x]) for ind, (nodes_file, ways_file) in enumerate(zip(nodes_files, ways_files)): - print('Processing File : \n{}\n{}'.format(ntpath.basename(nodes_file), ntpath.basename(ways_file))) + print('Processing File : {} , {}'.format(ntpath.basename(nodes_file), ntpath.basename(ways_file))) + mainLogger.info( + 'function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call,Processing File : {} {}'.format( + ntpath.basename(nodes_file), ntpath.basename(ways_file))) utild = UtilData(nodes_file, ways_file, cf) if cf.validation == 'intersectingvalidation': print("--" * 10) - print("intersectingvalidation") + print( + "Running intersectingvalidation to check if any ways which are intersecting have a missing intersection node") + mainLogger.info( + "function:main, step 0, 0 calls, 0 seconds, 0.000 seconds per call, Running intersectingvalidation to see if any ways which are intersecting have a missing intersecting node") + print("--" * 10) intersectLineStringInValidFormat(utild.ways_json, "brunnel", cf) if cf.do_eda: print("--" * 10) - print("eda") + print("Performing Exploratory Data Analysis") + mainLogger.info( + "function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing Exploratory Data Analysis") print("--" * 10) plot_nodes_vs_ways(utild, cf) subgraph_eda(utild, cf) if cf.do_all_validations: print("--" * 10) - print("all validations") + print("Performing Exploratory Data Analysis") + mainLogger.info( + "function:main, step 1, 1 calls, 0 seconds, 0.000 seconds per call, Performing all validations") print("--" * 10) get_invalidNodes(utild, cf) diff --git a/node_connectivity.py b/node_connectivity.py index 6955abb..21a94aa 100644 --- a/node_connectivity.py +++ b/node_connectivity.py @@ -1,3 +1,7 @@ +import logging +import time +from logging.config import fileConfig + import numpy as np import pandas as pd import json @@ -5,11 +9,13 @@ import networkx as nx import os import ntpath -import time - #####EDA Plots +from timerLog import timecall + + +@timecall(log_name='EDA', log_level=logging.INFO, immediate=False, messages="step4") def plot_nodes_vs_ways(utild, cf): """ Plot frequency distribution of #Nodes in each way @@ -33,18 +39,29 @@ def plot_nodes_vs_ways(utild, cf): plt.clf() +@timecall(log_name='EDA', log_level=logging.INFO, immediate=False, messages="step3") def subgraph_eda(utild, cf): """ Calculates the number of subgraphs from the given ways_list Plots a subraph and it's edges """ + starting = time.monotonic() print("Number of ways in the file : ", len(utild.ways_list)) print("Number of isolated ways: ", len(utild.disconnected_ways['features'])) print("Number of Connected ways: ", len(utild.connected_ways['features'])) + fileConfig('logging_config.ini') + log = logging.getLogger("EDA") connected_df = utild.ways_df connected_FG = nx.from_pandas_edgelist(connected_df, source='origin', target='dest') - print("Number of Connected Components : ", nx.number_connected_components(connected_FG)) + print("", ) + + log.info( + "function: subgraph_eda, step 3, 1 calls, {:.3} seconds, {:.3} seconds per call, Number of ways in the file : {} Number of isolated ways: {} Number of Connected ways: {} Number of Connected Components : {}".format( + time.monotonic() - starting, time.monotonic() - starting, len(utild.ways_list), + len(utild.disconnected_ways['features']), + len(utild.connected_ways['features']), nx.number_connected_components(connected_FG))) + subgraphs = [connected_FG.subgraph(c).copy() for c in nx.connected_components(connected_FG)] for i in range(len(subgraphs)): @@ -57,6 +74,8 @@ def subgraph_eda(utild, cf): print("sgraph[{}]_ways {}".format(i, ways_set)) break + +@timecall(log_name='EDA', log_level=logging.INFO, immediate=False, messages="step2") def get_way_from_subgraph(sgraph, df): """ Networkx gives subgraphs. This function is to infer the 'way id' from the given subgraph. @@ -75,7 +94,7 @@ def get_way_from_subgraph(sgraph, df): #### Validations - +@timecall(log_name='EDA', log_level=logging.INFO, immediate=False, messages="step1") def get_invalidNodes(utild, cf): """ A node is invalid if it is not part of any way and has no property assigned to it. diff --git a/requirements.txt b/requirements.txt index e7b56a7..66023b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ networkx==2.4 numpy==1.19.1 pandas==1.1.0 matplotlib==3.2.2 -jsonschema==3.2.0 \ No newline at end of file +jsonschema==3.2.0 +profilehooks==1.12.0 diff --git a/timerLog.py b/timerLog.py new file mode 100644 index 0000000..eb9c7b1 --- /dev/null +++ b/timerLog.py @@ -0,0 +1,864 @@ +""" +Profiling hooks + +This module contains a couple of decorators (`profile` and `coverage`) that +can be used to wrap functions and/or methods to produce profiles and line +coverage reports. There's a third convenient decorator (`timecall`) that +measures the duration of function execution without the extra profiling +overhead. + +Usage example:: + + from profilehooks import profile, coverage + + @profile # or @coverage + def fn(n): + if n < 2: return 1 + else: return n * fn(n-1) + + print(fn(42)) + +Or without imports, with some hack + + $ python -m profilehooks yourmodule + + @profile # or @coverage + def fn(n): + if n < 2: return 1 + else: return n * fn(n-1) + + print(fn(42)) + +Reports for all thusly decorated functions will be printed to sys.stdout +on program termination. You can alternatively request for immediate +reports for each call by passing immediate=True to the profile decorator. + +There's also a @timecall decorator for printing the time to sys.stderr +every time a function is called, when you just want to get a rough measure +instead of a detailed (but costly) profile. + +Caveats + + A thread on python-dev convinced me that hotshot produces bogus numbers. + See https://mail.python.org/pipermail/python-dev/2005-November/058264.html + + I don't know what will happen if a decorated function will try to call + another decorated function. All decorators probably need to explicitly + support nested profiling (currently TraceFuncCoverage is the only one + that supports this, while HotShotFuncProfile has support for recursive + functions.) + + Profiling with hotshot creates temporary files (*.prof for profiling, + *.cprof for coverage) in the current directory. These files are not + cleaned up. Exception: when you specify a filename to the profile + decorator (to store the pstats.Stats object for later inspection), + the temporary file will be the filename you specified with '.raw' + appended at the end. + + Coverage analysis with hotshot seems to miss some executions resulting + in lower line counts and some lines errorneously marked as never + executed. For this reason coverage analysis now uses trace.py which is + slower, but more accurate. + +Copyright (c) 2004--2020 Marius Gedminas +Copyright (c) 2007 Hanno Schlichting +Copyright (c) 2008 Florian Schulze + +Released under the MIT licence since December 2006: + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +(Previously it was distributed under the GNU General Public Licence.) +""" +from __future__ import print_function + +__author__ = "Marius Gedminas " +__copyright__ = "Copyright 2004-2020 Marius Gedminas and contributors" +__license__ = "MIT" +__version__ = '1.12.0' +__date__ = "2020-08-20" + +import atexit + +import functools +import inspect +import logging +import os +import re +import sys + +# For profiling +from profile import Profile +import pstats + +# For timecall +import timeit + +# For hotshot profiling (inaccurate!) +try: + import hotshot + import hotshot.stats +except ImportError: + hotshot = None + +# For trace.py coverage +import trace +import dis +import token +import tokenize + +# For hotshot coverage (inaccurate!; uses undocumented APIs; might break) +if hotshot is not None: + import _hotshot + import hotshot.log + +# For cProfile profiling (best) +try: + import cProfile +except ImportError: + cProfile = None + +# registry of available profilers +AVAILABLE_PROFILERS = {} + +__all__ = ['coverage', 'coverage_with_hotshot', 'profile', 'timecall'] + +# Use tokenize.open() on Python >= 3.2, fall back to open() on Python 2 +tokenize_open = getattr(tokenize, 'open', open) + + +def _unwrap(fn): + # inspect.unwrap() doesn't exist on Python 2 + if not hasattr(fn, '__wrapped__'): + return fn + else: + # intentionally using recursion here instead of a while loop to + # make cycles fail with a recursion error instead of looping forever. + return _unwrap(fn.__wrapped__) + + +def _identify(fn): + fn = _unwrap(fn) + funcname = fn.__name__ + filename = fn.__code__.co_filename + lineno = fn.__code__.co_firstlineno + return (funcname, filename, lineno) + + +def _is_file_like(o): + return hasattr(o, 'write') + + +def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False, + sort=None, entries=40, + profiler=('cProfile', 'profile', 'hotshot'), + stdout=True): + """Mark `fn` for profiling. + + If `skip` is > 0, first `skip` calls to `fn` will not be profiled. + + If `stdout` is not file-like and truthy, output will be printed to + sys.stdout. If it is a file-like object, output will be printed to it + instead. `stdout` must be writable in text mode (as opposed to binary) + if it is file-like. + + If `immediate` is False, profiling results will be printed to + self.stdout on program termination. Otherwise results will be printed + after each call. (If you don't want this, set stdout=False and specify a + `filename` to store profile data.) + + If `dirs` is False only the name of the file will be printed. + Otherwise the full path is used. + + `sort` can be a list of sort keys (defaulting to ['cumulative', + 'time', 'calls']). The following ones are recognized:: + + 'calls' -- call count + 'cumulative' -- cumulative time + 'file' -- file name + 'line' -- line number + 'module' -- file name + 'name' -- function name + 'nfl' -- name/file/line + 'pcalls' -- call count + 'stdname' -- standard name + 'time' -- internal time + + `entries` limits the output to the first N entries. + + `profiler` can be used to select the preferred profiler, or specify a + sequence of them, in order of preference. The default is ('cProfile'. + 'profile', 'hotshot'). + + If `filename` is specified, the profile stats will be stored in the + named file. You can load them with pstats.Stats(filename) or use a + visualization tool like RunSnakeRun. + + Usage:: + + def fn(...): + ... + fn = profile(fn, skip=1) + + If you are using Python 2.4, you should be able to use the decorator + syntax:: + + @profile(skip=3) + def fn(...): + ... + + or just :: + + @profile + def fn(...): + ... + + """ + if fn is None: # @profile() syntax -- we are a decorator maker + def decorator(fn): + return profile(fn, skip=skip, filename=filename, + immediate=immediate, dirs=dirs, + sort=sort, entries=entries, + profiler=profiler, stdout=stdout) + + return decorator + # @profile syntax -- we are a decorator. + if isinstance(profiler, str): + profiler = [profiler] + for p in profiler: + if p in AVAILABLE_PROFILERS: + profiler_class = AVAILABLE_PROFILERS[p] + break + else: + raise ValueError('only these profilers are available: %s' + % ', '.join(sorted(AVAILABLE_PROFILERS))) + fp = profiler_class(fn, skip=skip, filename=filename, + immediate=immediate, dirs=dirs, + sort=sort, entries=entries, stdout=stdout) + + # We cannot return fp or fp.__call__ directly as that would break method + # definitions, instead we need to return a plain function. + + @functools.wraps(fn) + def new_fn(*args, **kw): + return fp(*args, **kw) + + return new_fn + + +def coverage(fn): + """Mark `fn` for line coverage analysis. + + Results will be printed to sys.stdout on program termination. + + Usage:: + + def fn(...): + ... + fn = coverage(fn) + + If you are using Python 2.4, you should be able to use the decorator + syntax:: + + @coverage + def fn(...): + ... + + """ + fp = TraceFuncCoverage(fn) # or HotShotFuncCoverage + + # We cannot return fp or fp.__call__ directly as that would break method + # definitions, instead we need to return a plain function. + + @functools.wraps(fn) + def new_fn(*args, **kw): + return fp(*args, **kw) + + return new_fn + + +def coverage_with_hotshot(fn): + """Mark `fn` for line coverage analysis. + + Uses the 'hotshot' module for fast coverage analysis. + + BUG: Produces inaccurate results. + + See the docstring of `coverage` for usage examples. + """ + fp = HotShotFuncCoverage(fn) + + # We cannot return fp or fp.__call__ directly as that would break method + # definitions, instead we need to return a plain function. + + @functools.wraps(fn) + def new_fn(*args, **kw): + return fp(*args, **kw) + + return new_fn + + +class FuncProfile(object): + """Profiler for a function (uses profile).""" + + # This flag is shared between all instances + in_profiler = False + + Profile = Profile + + def __init__(self, fn, skip=0, filename=None, immediate=False, dirs=False, + sort=None, entries=40, stdout=True): + """Creates a profiler for a function. + + Every profiler has its own log file (the name of which is derived + from the function name). + + FuncProfile registers an atexit handler that prints profiling + information to sys.stderr when the program terminates. + """ + self.fn = fn + self.skip = skip + self.filename = filename + self._immediate = immediate + self.stdout = stdout + self._stdout_is_fp = self.stdout and _is_file_like(self.stdout) + self.dirs = dirs + self.sort = sort or ('cumulative', 'time', 'calls') + if isinstance(self.sort, str): + self.sort = (self.sort,) + self.entries = entries + self.reset_stats() + if not self.immediate: + atexit.register(self.atexit) + + @property + def immediate(self): + return self._immediate + + def __call__(self, *args, **kw): + """Profile a singe call to the function.""" + self.ncalls += 1 + if self.skip > 0: + self.skip -= 1 + self.skipped += 1 + return self.fn(*args, **kw) + if FuncProfile.in_profiler: + # handle recursive calls + return self.fn(*args, **kw) + # You cannot reuse the same profiler for many calls and accumulate + # stats that way. :-/ + profiler = self.Profile() + try: + FuncProfile.in_profiler = True + return profiler.runcall(self.fn, *args, **kw) + finally: + FuncProfile.in_profiler = False + self.stats.add(profiler) + if self.immediate: + self.print_stats() + self.reset_stats() + + def print_stats(self): + """Print profile information to sys.stdout.""" + stats = self.stats + if self.filename: + stats.dump_stats(self.filename) + if self.stdout: + funcname, filename, lineno = _identify(self.fn) + print_f = print + if self._stdout_is_fp: + print_f = functools.partial(print, file=self.stdout) + + print_f("") + print_f("*** PROFILER RESULTS ***") + print_f("%s (%s:%s)" % (funcname, filename, lineno)) + if self.skipped: + skipped = " (%d calls not profiled)" % self.skipped + else: + skipped = "" + print_f("function called %d times%s" % (self.ncalls, skipped)) + print_f("") + if not self.dirs: + stats.strip_dirs() + stats.sort_stats(*self.sort) + stats.print_stats(self.entries) + + def reset_stats(self): + """Reset accumulated profiler statistics.""" + # send stats printing to specified stdout if it's file-like + stream = self.stdout if self._stdout_is_fp else sys.stdout + + # Note: not using self.Profile, since pstats.Stats() fails then + self.stats = pstats.Stats(Profile(), stream=stream) + self.ncalls = 0 + self.skipped = 0 + + def atexit(self): + """Stop profiling and print profile information to sys.stdout or self.stdout. + + This function is registered as an atexit hook. + """ + self.print_stats() + + +AVAILABLE_PROFILERS['profile'] = FuncProfile + +if cProfile is not None: + class CProfileFuncProfile(FuncProfile): + """Profiler for a function (uses cProfile).""" + + Profile = cProfile.Profile + + + AVAILABLE_PROFILERS['cProfile'] = CProfileFuncProfile + +if hotshot is not None: + + class HotShotFuncProfile(FuncProfile): + """Profiler for a function (uses hotshot).""" + + # This flag is shared between all instances + in_profiler = False + + def __init__(self, fn, skip=0, filename=None, immediate=False, + dirs=False, sort=None, entries=40, stdout=True): + """Creates a profiler for a function. + + Every profiler has its own log file (the name of which is derived + from the function name). + + HotShotFuncProfile registers an atexit handler that prints + profiling information to sys.stderr when the program terminates. + + The log file is not removed and remains there to clutter the + current working directory. + """ + if filename: + self.logfilename = filename + ".raw" + else: + self.logfilename = "%s.%d.prof" % (fn.__name__, os.getpid()) + super(HotShotFuncProfile, self).__init__( + fn, skip=skip, filename=filename, immediate=immediate, + dirs=dirs, sort=sort, entries=entries, stdout=stdout) + + def __call__(self, *args, **kw): + """Profile a singe call to the function.""" + self.ncalls += 1 + if self.skip > 0: + self.skip -= 1 + self.skipped += 1 + return self.fn(*args, **kw) + if HotShotFuncProfile.in_profiler: + # handle recursive calls + return self.fn(*args, **kw) + if self.profiler is None: + self.profiler = hotshot.Profile(self.logfilename) + try: + HotShotFuncProfile.in_profiler = True + return self.profiler.runcall(self.fn, *args, **kw) + finally: + HotShotFuncProfile.in_profiler = False + if self.immediate: + self.print_stats() + self.reset_stats() + + def print_stats(self): + if self.profiler is None: + self.stats = pstats.Stats(Profile()) + else: + self.profiler.close() + self.stats = hotshot.stats.load(self.logfilename) + super(HotShotFuncProfile, self).print_stats() + + def reset_stats(self): + self.profiler = None + self.ncalls = 0 + self.skipped = 0 + + + AVAILABLE_PROFILERS['hotshot'] = HotShotFuncProfile + + + class HotShotFuncCoverage: + """Coverage analysis for a function (uses _hotshot). + + HotShot coverage is reportedly faster than trace.py, but it appears to + have problems with exceptions; also line counts in coverage reports + are generally lower from line counts produced by TraceFuncCoverage. + Is this my bug, or is it a problem with _hotshot? + """ + + def __init__(self, fn): + """Creates a profiler for a function. + + Every profiler has its own log file (the name of which is derived + from the function name). + + HotShotFuncCoverage registers an atexit handler that prints + profiling information to sys.stderr when the program terminates. + + The log file is not removed and remains there to clutter the + current working directory. + """ + self.fn = fn + self.logfilename = "%s.%d.cprof" % (fn.__name__, os.getpid()) + self.profiler = _hotshot.coverage(self.logfilename) + self.ncalls = 0 + atexit.register(self.atexit) + + def __call__(self, *args, **kw): + """Profile a singe call to the function.""" + self.ncalls += 1 + old_trace = sys.gettrace() + try: + return self.profiler.runcall(self.fn, args, kw) + finally: # pragma: nocover + sys.settrace(old_trace) + + def atexit(self): + """Stop profiling and print profile information to sys.stderr. + + This function is registered as an atexit hook. + """ + self.profiler.close() + funcname, filename, lineno = _identify(self.fn) + print("") + print("*** COVERAGE RESULTS ***") + print("%s (%s:%s)" % (funcname, filename, lineno)) + print("function called %d times" % self.ncalls) + print("") + fs = FuncSource(self.fn) + reader = hotshot.log.LogReader(self.logfilename) + for what, (filename, lineno, funcname), tdelta in reader: + if filename != fs.filename: + continue + if what == hotshot.log.LINE: + fs.mark(lineno) + if what == hotshot.log.ENTER: + # hotshot gives us the line number of the function + # definition and never gives us a LINE event for the first + # statement in a function, so if we didn't perform this + # mapping, the first statement would be marked as never + # executed + if lineno == fs.firstlineno: + lineno = fs.firstcodelineno + fs.mark(lineno) + reader.close() + print(fs) + never_executed = fs.count_never_executed() + if never_executed: + print("%d lines were not executed." % never_executed) + + +class TraceFuncCoverage: + """Coverage analysis for a function (uses trace module). + + HotShot coverage analysis is reportedly faster, but it appears to have + problems with exceptions. + """ + + # Shared between all instances so that nested calls work + tracer = trace.Trace(count=True, trace=False, + ignoredirs=[sys.prefix, sys.exec_prefix]) + + # This flag is also shared between all instances + tracing = False + + def __init__(self, fn): + """Creates a profiler for a function. + + Every profiler has its own log file (the name of which is derived + from the function name). + + TraceFuncCoverage registers an atexit handler that prints + profiling information to sys.stderr when the program terminates. + + The log file is not removed and remains there to clutter the + current working directory. + """ + self.fn = fn + self.logfilename = "%s.%d.cprof" % (fn.__name__, os.getpid()) + self.ncalls = 0 + atexit.register(self.atexit) + + def __call__(self, *args, **kw): + """Profile a singe call to the function.""" + self.ncalls += 1 + if TraceFuncCoverage.tracing: # pragma: nocover + return self.fn(*args, **kw) + old_trace = sys.gettrace() + try: + TraceFuncCoverage.tracing = True + return self.tracer.runfunc(self.fn, *args, **kw) + finally: # pragma: nocover + sys.settrace(old_trace) + TraceFuncCoverage.tracing = False + + def atexit(self): + """Stop profiling and print profile information to sys.stderr. + + This function is registered as an atexit hook. + """ + funcname, filename, lineno = _identify(self.fn) + print("") + print("*** COVERAGE RESULTS ***") + print("%s (%s:%s)" % (funcname, filename, lineno)) + print("function called %d times" % self.ncalls) + print("") + fs = FuncSource(self.fn) + for (filename, lineno), count in self.tracer.counts.items(): + if filename != fs.filename: + continue + fs.mark(lineno, count) + print(fs) + never_executed = fs.count_never_executed() + if never_executed: + print("%d lines were not executed." % never_executed) + + +class FuncSource: + """Source code annotator for a function.""" + + blank_rx = re.compile(r"^\s*finally:\s*(#.*)?$") + + def __init__(self, fn): + self.fn = fn + self.filename = inspect.getsourcefile(fn) + self.sourcelines = {} + self.source = [] + self.firstlineno = self.firstcodelineno = 0 + try: + self.source, self.firstlineno = inspect.getsourcelines(fn) + self.firstcodelineno = self.firstlineno + self.find_source_lines() + except IOError: + self.filename = None + + def find_source_lines(self): + """Mark all executable source lines in fn as executed 0 times.""" + if self.filename is None: + return + strs = self._find_docstrings(self.filename) + lines = { + ln + for off, ln in dis.findlinestarts(_unwrap(self.fn).__code__) + if ln not in strs + } + for lineno in lines: + self.sourcelines.setdefault(lineno, 0) + if lines: + self.firstcodelineno = min(lines) + else: # pragma: nocover + # This branch cannot be reached, I'm just being paranoid. + self.firstcodelineno = self.firstlineno + + def _find_docstrings(self, filename): + # A replacement for trace.find_strings() which was deprecated in + # Python 3.2 and removed in 3.6. + strs = set() + prev = token.INDENT # so module docstring is detected as docstring + with tokenize_open(filename) as f: + tokens = tokenize.generate_tokens(f.readline) + for ttype, tstr, start, end, line in tokens: + if ttype == token.STRING and prev == token.INDENT: + strs.update(range(start[0], end[0] + 1)) + prev = ttype + return strs + + def mark(self, lineno, count=1): + """Mark a given source line as executed count times. + + Multiple calls to mark for the same lineno add up. + """ + self.sourcelines[lineno] = self.sourcelines.get(lineno, 0) + count + + def count_never_executed(self): + """Count statements that were never executed.""" + lineno = self.firstlineno + counter = 0 + for line in self.source: + if self.sourcelines.get(lineno) == 0: + if not self.blank_rx.match(line): + counter += 1 + lineno += 1 + return counter + + def __str__(self): + """Return annotated source code for the function.""" + if self.filename is None: + return "cannot show coverage data since co_filename is None" + lines = [] + lineno = self.firstlineno + for line in self.source: + counter = self.sourcelines.get(lineno) + if counter is None: + prefix = ' ' * 7 + elif counter == 0: + if self.blank_rx.match(line): # pragma: nocover + # This is an workaround for an ancient bug I can't + # reproduce, perhaps because it was fixed, or perhaps + # because I can't remember all the details. + prefix = ' ' * 7 + else: + prefix = '>' * 6 + ' ' + else: + prefix = '%5d: ' % counter + lines.append(prefix + line) + lineno += 1 + return ''.join(lines) + + +def timecall( + fn=None, immediate=True, timer=None, + log_name=None, log_level=logging.DEBUG, messages=None +): + """Wrap `fn` and print its execution time. + + Example:: + + @timecall + def somefunc(x, y): + time.sleep(x * y) + + somefunc(2, 3) + + will print the time taken by somefunc on every call. If you want just + a summary at program termination, use :: + + @timecall(immediate=False) + + You can also choose a timing method other than the default + ``timeit.default_timer()``, e.g.:: + + @timecall(timer=time.clock) + + You can also log the output to a logger by specifying the name and level + of the logger to use, eg: + + @timecall(immediate=True, + log_name='profile_log', + log_level=logging.DEBUG) + + """ + if fn is None: # @timecall() syntax -- we are a decorator maker + def decorator(fn): + return timecall( + fn, immediate=immediate, timer=timer, + log_name=log_name, log_level=log_level, messages=messages + ) + + return decorator + # @timecall syntax -- we are a decorator. + if timer is None: + timer = timeit.default_timer + fp = FuncTimer( + fn, immediate=immediate, timer=timer, + log_name=log_name, log_level=log_level, messages=messages + ) + + # We cannot return fp or fp.__call__ directly as that would break method + # definitions, instead we need to return a plain function. + + @functools.wraps(fn) + def new_fn(*args, **kw): + return fp(*args, **kw) + + return new_fn + + +class FuncTimer(object): + + def __init__( + self, fn, immediate, timer, + log_name=None, log_level=logging.DEBUG, messages="" + ): + self.msg = messages + self.logger = None + if log_name: + self.logger = logging.getLogger(log_name) + self.log_level = log_level + self.fn = fn + self.ncalls = 0 + self.totaltime = 0 + self.immediate = immediate + self.timer = timer + if not immediate: + atexit.register(self.atexit) + + def __call__(self, *args, **kw): + """Profile a singe call to the function.""" + fn = self.fn + timer = self.timer + self.ncalls += 1 + start = timer() + try: + return fn(*args, **kw) + finally: + duration = timer() - start + self.totaltime += duration + if self.immediate: + funcname, filename, lineno = _identify(fn) + message = "%s %.3f seconds" % ( + funcname, duration, + ) + if self.logger: + self.logger.log(self.log_level, message) + else: + sys.stderr.write("\n " + message) + sys.stderr.flush() + + def atexit(self): + if not self.ncalls: + return + funcname, filename, lineno = _identify(self.fn) + message = "function: %s, %s, %d calls, %.3f seconds, %.3f seconds per call, No Comments" % ( + funcname, self.msg, self.ncalls, + self.totaltime, self.totaltime / self.ncalls) + if self.logger: + self.logger.log(self.log_level, message) + else: + print(message) + + +if __name__ == '__main__': + + local = dict((name, globals()[name]) for name in __all__) + message = """******** +Injected `profilehooks` +-------- +{} +******** +""".format("\n".join(local.keys())) + + + def interact_(): + from code import interact + interact(message, local=local) + + + def run_(): + from runpy import run_module + print(message) + run_module(sys.argv[1], init_globals=local) + + + if len(sys.argv) == 1: + interact_() + else: + run_() diff --git a/util_data.py b/util_data.py index 4f542b2..8092dee 100644 --- a/util_data.py +++ b/util_data.py @@ -5,8 +5,8 @@ Any validations that need to READ-ONLY this data can be defined as members outside the class. Any validations that needs to UPDATE the data shoulb be defined as members of the class UtilData. -Tentative Sequence of operations : - +Tentative Sequence of operations : + #Build the Utildata 0. Read the nodes and ways files as json objects 1. Build nodes and ways list @@ -15,7 +15,7 @@ 4. Build coordinate df (for subgraps) 5. Get isolated ways 6. Split json file into connected and disconnected - + #EDA ''' 01. Plot #Nodes vs #Ways @@ -29,8 +29,11 @@ ''' """ +import logging import os import json +from logging.config import fileConfig + import matplotlib.pyplot as plt import pandas as pd import numpy as np @@ -38,6 +41,8 @@ import time import networkx as nx +from timerLog import timecall + class UtilData: def __init__(self, nodes_file, ways_file, cf): @@ -71,6 +76,7 @@ def __init__(self, nodes_file, ways_file, cf): self.split_ways_geojson_file(cf) self.get_coord_df() + @timecall(log_name='utildata', log_level=logging.INFO, immediate=False, messages="step6") def get_coords_list(self, features_list, cf): """ Returns list of list of coordinates. @@ -88,6 +94,7 @@ def get_coords_list(self, features_list, cf): coord_list.append(elem['geometry']['coordinates']) return coord_list + @timecall(log_name='utildata', log_level=logging.INFO, immediate=False, messages="step5") def get_coord_dict(self): """ Returns dictionary of coordinates @@ -107,6 +114,7 @@ def get_coord_dict(self): coord_dict[str(point)].append(id) self.coord_dict = coord_dict + @timecall(log_name='utildata', log_level=logging.INFO, immediate=False, messages="step4") def get_isolated_ways(self): """ A way is isolated if none of it's nodes are part of any other way @@ -124,6 +132,7 @@ def get_isolated_ways(self): isolated_way_ids.update([id]) self.isolated_way_ids = isolated_way_ids + @timecall(log_name='utildata', log_level=logging.INFO, immediate=False, messages="step3") def split_ways_geojson_file(self, cf): """ Splits the geojson file based on connectivity with with other ways @@ -153,9 +162,10 @@ def split_ways_geojson_file(self, cf): json.dump(connected_ways, fp, indent=4) with open(disconnected_save_path, 'w') as fp: json.dump(disconnected_ways, fp, indent=4) - print("ways_file split into \n{} and \n{}".format(ntpath.basename(connected_save_path), - ntpath.basename(disconnected_save_path))) + print("\n The ways_file split into: \n {} and {}".format(ntpath.basename(connected_save_path), + ntpath.basename(disconnected_save_path))) + @timecall(log_name='utildata', log_level=logging.INFO, immediate=False, messages="step2") def get_coord_df(self): """ Return df with two columns origin and dest for starting and ending nodes of the way @@ -167,6 +177,7 @@ def get_coord_df(self): df = df.append({'origin': str(elem[0]), 'dest': str(elem[-1])}, ignore_index=True) self.ways_df = df + @timecall(log_name='utildata', log_level=logging.INFO, immediate=False, messages="step1") def get_one_node_ways(self): """ Geojson Geometry type validations : https://tools.ietf.org/html/rfc7946#section-3.1.4