| 1 | #! /usr/bin/env python |
|---|
| 2 | from ppclass import pp |
|---|
| 3 | from ppplot import plot2d,save,writeascii |
|---|
| 4 | import numpy as np |
|---|
| 5 | from scipy.ndimage.measurements import minimum_position |
|---|
| 6 | import matplotlib.pyplot as mpl |
|---|
| 7 | import ppcompute |
|---|
| 8 | |
|---|
| 9 | ############################################################################### |
|---|
| 10 | # FIND DUST DEVILS |
|---|
| 11 | # filefile --> file |
|---|
| 12 | # dx --> horizontal resolution |
|---|
| 13 | # dt_out --> frequency of outputs (s) |
|---|
| 14 | # lt_start --> starting local time |
|---|
| 15 | # halolim --> limit for halos |
|---|
| 16 | # method --> method used to detect vortices |
|---|
| 17 | # plotplot --> plot or not |
|---|
| 18 | # save --> save results or not |
|---|
| 19 | ############################################################################### |
|---|
| 20 | def finddd(filefile,\ |
|---|
| 21 | timelist,\ |
|---|
| 22 | dx=50.,\ |
|---|
| 23 | dt_out=50.,\ |
|---|
| 24 | lt_start=8.,\ |
|---|
| 25 | halolim=30.,\ |
|---|
| 26 | method=1,\ |
|---|
| 27 | plotplot=False,\ |
|---|
| 28 | save=True): |
|---|
| 29 | |
|---|
| 30 | ############################################################################### |
|---|
| 31 | ########################## FOR METHOD 1 FOR METHOD 1 ########################## |
|---|
| 32 | ## FACLIST : how many sigmas below mean we start to consider this could be a vortex |
|---|
| 33 | ## ... < 3.0 dubious low-intensity minima are caught |
|---|
| 34 | ## ... 3 ~ 3.1 is a bit too low but helps? because size of pressure drop could be an underestimate of actual dust devil |
|---|
| 35 | ## ... 3.2 ~ 3.3 is probably right, this is the one we choose for exploration [3.25] |
|---|
| 36 | ## ... NB: values 3.2 to 3.6 yields the same number of devils but measured sizes could vary a bit |
|---|
| 37 | ## ... > 3.6 makes strong minima disappear... especially >3.8 |
|---|
| 38 | ## ... EVENTUALLY 3.5-4 is better for problematic cases |
|---|
| 39 | ############################################################################### |
|---|
| 40 | faclist = [3.75] |
|---|
| 41 | |
|---|
| 42 | ## (see below) NEIGHBOR_FAC is the multiple of std used to evaluate size |
|---|
| 43 | ## --> 1: limit not discriminative enough. plus does not separate neighbouring vortices. |
|---|
| 44 | ## ... but interesting: gives an exponential law (because vortices are artificially merged?) |
|---|
| 45 | ## --> 2.7: very good for method 1. corresponds usually to ~0.3 |
|---|
| 46 | ## --> 2: so-so. do not know what to think. |
|---|
| 47 | neighbor_fac = 2.7 |
|---|
| 48 | |
|---|
| 49 | ############################################################################### |
|---|
| 50 | if save: |
|---|
| 51 | myfile1 = open(filefile+'m'+str(method)+'_'+'1.txt', 'w') |
|---|
| 52 | myfile2 = open(filefile+'m'+str(method)+'_'+'2.txt', 'w') |
|---|
| 53 | ############################################################################### |
|---|
| 54 | |
|---|
| 55 | ## mean and std calculations |
|---|
| 56 | ## -- std is used in both methods for limits |
|---|
| 57 | ## -- mean is only used in method 1 |
|---|
| 58 | print "calculate mean and std, please wait." |
|---|
| 59 | ## -- get time series of 2D surface pressure |
|---|
| 60 | psfc = pp(file=filefile,var="PSFC").getf() |
|---|
| 61 | ## -- calculate mean and standard deviation |
|---|
| 62 | ## -- ... calculating std at all time is not right! |
|---|
| 63 | ## -- ... for mean value though, similar results with both methods |
|---|
| 64 | mean = np.mean(psfc,dtype=np.float64) |
|---|
| 65 | std = np.std(psfc,dtype=np.float64) |
|---|
| 66 | ## some information about inferred limits |
|---|
| 67 | print "**************************************************************" |
|---|
| 68 | print "MEAN",mean |
|---|
| 69 | print "STD",std |
|---|
| 70 | print "LIMIT FOR PRESSURE MINIMUM:",-np.array(faclist)*std |
|---|
| 71 | print "LIMIT FOR EVALUATING SIZE OF A GIVEN LOCAL MINIMUM",-neighbor_fac*std |
|---|
| 72 | print "**************************************************************" |
|---|
| 73 | |
|---|
| 74 | ## LOOP ON TIME |
|---|
| 75 | for time in timelist: |
|---|
| 76 | |
|---|
| 77 | ## get 2D surface pressure at a given time |
|---|
| 78 | ## (this is actually so quick we don't use psfc above) |
|---|
| 79 | psfc2d = pp(file=filefile,var="PSFC",t=time).getf() |
|---|
| 80 | #ustm = pp(file=filefile,var="USTM",t=time).getf() |
|---|
| 81 | |
|---|
| 82 | ## MAIN ANALYSIS. LOOP ON FAC. |
|---|
| 83 | for fac in faclist: |
|---|
| 84 | #fac = 3.75 |
|---|
| 85 | #for method in [2,1]: |
|---|
| 86 | |
|---|
| 87 | ## initialize arrays |
|---|
| 88 | tabij = [] ; tabsize = [] ; tabdrop = [] |
|---|
| 89 | tabijcenter = [] ; tabijvortex = [] ; tabijnotconv = [] ; tabdim = [] |
|---|
| 90 | |
|---|
| 91 | ################ FIND RELEVANT POINTS TO BE ANALYZED |
|---|
| 92 | ## lab is 1 for points to be treated by minimum_position routine |
|---|
| 93 | ## we set elements at 1 where pressure is under mean-fac*std |
|---|
| 94 | ## otherwise we set to 0 because this means we are close enough to mean pressure (background) |
|---|
| 95 | lab = np.zeros(psfc2d.shape) |
|---|
| 96 | if method == 1: |
|---|
| 97 | # method 1: standard deviation |
|---|
| 98 | lab[np.where(psfc2d < mean-fac*std)] = 1 |
|---|
| 99 | else: |
|---|
| 100 | # method 2: polynomial fit |
|---|
| 101 | # ... tried smooth but too difficult, not accurate and too expensive |
|---|
| 102 | deg = 5 #plutot bien (deg 10 ~pareil) mais loupe les gros (~conv cell) |
|---|
| 103 | #deg = 2 #pas mal mais false positive (pareil 3-4 mm si un peu mieux) |
|---|
| 104 | # #(OK now with fixing the false positive bug) |
|---|
| 105 | nx = psfc2d.shape[1] ; ny = psfc2d.shape[0] |
|---|
| 106 | xxx = np.array(range(nx)) ; yyy = np.array(range(ny)) |
|---|
| 107 | anopsfc2d = psfc2d*0. ; polypsfc2d = psfc2d*0. |
|---|
| 108 | for iii in range(0,nx,1): |
|---|
| 109 | poly = np.poly1d(np.polyfit(yyy,psfc2d[iii,:],deg)) |
|---|
| 110 | polypsfc2d[iii,:] = poly(yyy) |
|---|
| 111 | for jjj in range(0,ny,1): |
|---|
| 112 | poly = np.poly1d(np.polyfit(xxx,psfc2d[:,jjj],deg)) |
|---|
| 113 | polypsfc2d[:,jjj] = 0.5*polypsfc2d[:,jjj] + 0.5*poly(xxx) |
|---|
| 114 | ## smooth a little to avoid 'crosses' (plus, this removes PBC problems) |
|---|
| 115 | polypsfc2d = ppcompute.smooth2diter(polypsfc2d,n=deg) |
|---|
| 116 | # compute anomaly and find points to be explored |
|---|
| 117 | anopsfc2d = psfc2d - polypsfc2d |
|---|
| 118 | limlim = fac*std ## same as method 1 |
|---|
| 119 | lab[np.where(anopsfc2d < -limlim)] = 1 |
|---|
| 120 | |
|---|
| 121 | ## while there are still points to be analyzed... |
|---|
| 122 | while 1 in lab: |
|---|
| 123 | ## ... get the point with the minimum field values |
|---|
| 124 | if method == 1: |
|---|
| 125 | ij = minimum_position(psfc2d,labels=lab) |
|---|
| 126 | else: |
|---|
| 127 | ij = minimum_position(anopsfc2d,labels=lab) |
|---|
| 128 | ## ... store the indexes of the point in tabij |
|---|
| 129 | tabij.append(ij) |
|---|
| 130 | ## ... remove the point from labels to be further explored by minimum_position |
|---|
| 131 | lab[ij] = 0 |
|---|
| 132 | |
|---|
| 133 | ################ GET SIZES BASED ON THOSE FOUND POINTS |
|---|
| 134 | ## reslab is the same as lab |
|---|
| 135 | ## except for scanning purpose we keep the information |
|---|
| 136 | ## about how went the detection |
|---|
| 137 | ## --> and we set a lower fac |
|---|
| 138 | ## --> above a high fac is good to catch only strong vortices |
|---|
| 139 | ## --> but here a casual fac=3 is better to get accurate sizes |
|---|
| 140 | ## --> or even lower as shown by plotting reslab |
|---|
| 141 | reslab = np.zeros(psfc2d.shape) |
|---|
| 142 | if method == 1: |
|---|
| 143 | reslab[np.where(psfc2d < mean-neighbor_fac*std)] = 1 |
|---|
| 144 | else: |
|---|
| 145 | reslab[np.where(anopsfc2d < -neighbor_fac*std)] = 1 |
|---|
| 146 | |
|---|
| 147 | ## initialize halomax and while loop |
|---|
| 148 | ## HALOMAX : maximum halo defined around a minima to evaluate the size |
|---|
| 149 | ## ... halomax must be large enough to encompass a vortex |
|---|
| 150 | ## ... but not too large otherwise neighboring vortex are caught |
|---|
| 151 | halomax = 3 ; notconv = 9999 ; yorgl = 9999 |
|---|
| 152 | |
|---|
| 153 | ## WHILE LOOP on HALOMAX exploration |
|---|
| 154 | while ( notconv > 0 and halomax < halolim and yorgl != 0 ): |
|---|
| 155 | # now browse through all points caught in previous loop |
|---|
| 156 | for ij in tabij: |
|---|
| 157 | ## ... OK. take each indexes found before with minimum_position |
|---|
| 158 | i,j = ij[0],ij[1] |
|---|
| 159 | ## EITHER |
|---|
| 160 | ## ... if reslab is already 0, we do not have to do anything |
|---|
| 161 | ## ... because this means point is already part of detected vortex |
|---|
| 162 | ## OR |
|---|
| 163 | ## ... if the ij couple is already in a previously detected vortex |
|---|
| 164 | ## ... we don't actually need to consider it again |
|---|
| 165 | ## ... this is necessary otherwise (sometimes a lot) of false positives |
|---|
| 166 | if reslab[i,j] <= 0 or ij in tabijvortex: |
|---|
| 167 | pass |
|---|
| 168 | else: |
|---|
| 169 | ## ... then define a growing halo around this point |
|---|
| 170 | ## ... we make the halo grow until convergence of |
|---|
| 171 | ## ... the number of under-limit points (i.e. with lab=1) within halo |
|---|
| 172 | ## ... which means the whole vortex is encompassed |
|---|
| 173 | ## ... we start with a halo of 1 point around minimum point |
|---|
| 174 | ## ... and we end when we reached halomax which likely means dubious case |
|---|
| 175 | halo = 1 ; nmesh = -9999. ; prevmesh = 9999. ; notconverged = False |
|---|
| 176 | while nmesh != prevmesh and halo <= halomax: |
|---|
| 177 | ## ... save the number of vortex points calculated at previous iteration |
|---|
| 178 | prevmesh = nmesh |
|---|
| 179 | ## ... define a halo around the minimum point |
|---|
| 180 | minx,maxx,miny,maxy = i-halo,i+halo+1,j-halo,j+halo+1 |
|---|
| 181 | ## ... treat the boundary case (TBD: periodic boundary conditions) |
|---|
| 182 | if minx < 0: minx = 0 |
|---|
| 183 | if miny < 0: miny = 0 |
|---|
| 184 | if maxx > psfc2d.shape[0]: maxx = psfc2d.shape[0] |
|---|
| 185 | if maxy > psfc2d.shape[1]: maxy = psfc2d.shape[1] |
|---|
| 186 | ## ... define the patch, made of the halo of points |
|---|
| 187 | patch = reslab[minx:maxx,miny:maxy] |
|---|
| 188 | ## ... count how many 1 are inside this patch |
|---|
| 189 | ## ... these are points for which value is >0 |
|---|
| 190 | ## ... because close to mean is 0 and already caught is <0 |
|---|
| 191 | ## ... and not converged are >1 so still to be scanned |
|---|
| 192 | nmesh = len(np.where(patch >= 1)[0]) |
|---|
| 193 | ## ... in case widening halo adds 1-2 points only |
|---|
| 194 | ## ... we consider vortex is encompassed |
|---|
| 195 | ## ... this helps to separate neighboring vortices |
|---|
| 196 | ## ... and only yields marginal inaccuracies |
|---|
| 197 | if halo > 3 and abs(nmesh-prevmesh)<=2: prevmesh = nmesh |
|---|
| 198 | ## ... if with halo=1 we caught a 1-point vortex, end here because spurious |
|---|
| 199 | if halo == 1 and nmesh == 1: prevmesh = nmesh |
|---|
| 200 | ## ... with the last halo=halomax test, we can detect not-converged cases |
|---|
| 201 | if halo == halomax and nmesh != prevmesh: notconverged = True |
|---|
| 202 | ## ... increment halo size before the next iteration |
|---|
| 203 | halo = halo + 1 |
|---|
| 204 | ## ... now we got the grid points encompassed by the vortex in nmesh |
|---|
| 205 | ## ... then to continue scanning we store results in reslab |
|---|
| 206 | if notconverged: |
|---|
| 207 | ## multiply reslab by 2. |
|---|
| 208 | ## --> if it is >1, point could be part of another vortex found later |
|---|
| 209 | ## --> if it is <0, point is already within a caught vortex, so reslab should remain <0 |
|---|
| 210 | ## --> if it is =0, this should remain 0 because close to average |
|---|
| 211 | reslab[i,j] = reslab[i,j]*2 |
|---|
| 212 | tabijnotconv.append(ij) |
|---|
| 213 | else: |
|---|
| 214 | ## OK. this is most likely an actual vortex. we get the drop. |
|---|
| 215 | ## we multiply by mesh area, then square to get approx. size of vortex |
|---|
| 216 | if method == 1: |
|---|
| 217 | drop = -psfc2d[i,j]+mean |
|---|
| 218 | else: |
|---|
| 219 | drop = -anopsfc2d[i,j] |
|---|
| 220 | size = int(np.sqrt(nmesh*dx*dx)) |
|---|
| 221 | ## if vortex is too small (i.e. too close to mesh grid resolution) we don't store information |
|---|
| 222 | ## ... we just remove the patch from labels to be further explored |
|---|
| 223 | facdx = 2. |
|---|
| 224 | if size < facdx*dx: |
|---|
| 225 | reslab[minx:maxx,miny:maxy] = 0 |
|---|
| 226 | ## otherwise it is a VORTEX! we store info in arrays |
|---|
| 227 | else: |
|---|
| 228 | ## we put the vortex points into tabijvortex so that |
|---|
| 229 | ## a (i,j) couple into a vortex is not considered in further explorations |
|---|
| 230 | ## -- also we evaluate the x-size and y-size of the vortex (max distance to center) |
|---|
| 231 | ## -- this can be useful to detect not-so-round fake vortices (convective gusts?) |
|---|
| 232 | maxw = -9999 ; maxh = -9999 ; maxu = -9999 |
|---|
| 233 | for iii in range(minx,maxx): |
|---|
| 234 | for jjj in range(miny,maxy): |
|---|
| 235 | if reslab[iii,jjj] != 0: |
|---|
| 236 | tabijvortex.append((iii,jjj)) |
|---|
| 237 | width = np.abs(iii-ij[0]) |
|---|
| 238 | if width > maxw: maxw = width |
|---|
| 239 | height = np.abs(jjj-ij[1]) |
|---|
| 240 | if height > maxh: maxh = height |
|---|
| 241 | #ustar = ustm[iii,jjj] |
|---|
| 242 | #if ustar > maxu: maxu = ustar |
|---|
| 243 | #print size,drop,ustar |
|---|
| 244 | ## store info in dedicated arrays |
|---|
| 245 | tabdim.append((maxw*dx,maxh*dx)) |
|---|
| 246 | tabsize.append(size) |
|---|
| 247 | tabdrop.append(drop) |
|---|
| 248 | tabijcenter.append(ij) |
|---|
| 249 | #print "... VORTEX!!!! size %.0f drop %.1f coord %.0f %.0f" % (size,drop,i,j) |
|---|
| 250 | ## we remove the patch from labels to be further explored |
|---|
| 251 | ## we multiply reslab by -1 to plot detection maps |
|---|
| 252 | reslab[minx:maxx,miny:maxy] = patch*-1 |
|---|
| 253 | |
|---|
| 254 | ## count how many points are not converged and left to be analyzed |
|---|
| 255 | notconv = len(np.where(reslab > 1)[0]) |
|---|
| 256 | yorgl = len(np.where(reslab == 1)[0]) |
|---|
| 257 | |
|---|
| 258 | ## increment halomax |
|---|
| 259 | ## to speed-up the increment is slightly increasing with considered halomax |
|---|
| 260 | halomax = halomax + halomax / 2 |
|---|
| 261 | |
|---|
| 262 | ## just for simpler plots. |
|---|
| 263 | reslab[reslab > 2] = 4 |
|---|
| 264 | reslab[reslab < -2] = -4 |
|---|
| 265 | |
|---|
| 266 | ## give some info to the user |
|---|
| 267 | if len(tabsize) > 0: |
|---|
| 268 | nvortex = len(tabsize) |
|---|
| 269 | maxsize = np.max(tabsize) |
|---|
| 270 | maxdrop = np.max(tabdrop) |
|---|
| 271 | else: |
|---|
| 272 | nvortex = 0 |
|---|
| 273 | maxsize = 0 |
|---|
| 274 | maxdrop = 0. |
|---|
| 275 | notconv = len(np.where(reslab > 1)[0]) |
|---|
| 276 | print "t=%3.0f / n=%2.0f / s_max=%4.0f / d_max=%4.1f / halo_out=%3.0f / notconvp=%3.1f" \ |
|---|
| 277 | % (time,nvortex,maxsize,maxdrop,halomax,100.*notconv/float(reslab.size)) |
|---|
| 278 | |
|---|
| 279 | ## save results in a text file |
|---|
| 280 | if save: |
|---|
| 281 | # convert t in local time |
|---|
| 282 | ttt = lt_start + time*dt_out/3700. |
|---|
| 283 | # write files |
|---|
| 284 | myfile2.write( "%5.2f ; %5.0f ; %5.0f ; %7.2f\n" % (ttt,nvortex,maxsize,maxdrop) ) |
|---|
| 285 | for iii in range(len(tabsize)): |
|---|
| 286 | myfile1.write( "%5.2f ; %5.0f ; %7.2f ; %5.0f ; %5.0f\n" \ |
|---|
| 287 | % (ttt,tabsize[iii],tabdrop[iii],tabdim[iii][0],tabdim[iii][1]) ) |
|---|
| 288 | |
|---|
| 289 | #### PLOT PLOT PLOT PLOT |
|---|
| 290 | if nvortex>0 and plotplot: |
|---|
| 291 | mpl.figure(figsize=(12,8)) |
|---|
| 292 | myplot = plot2d() |
|---|
| 293 | myplot.absc = np.array(range(psfc2d.shape[1]))*dx/1000. |
|---|
| 294 | myplot.ordi = np.array(range(psfc2d.shape[0]))*dx/1000. |
|---|
| 295 | myplot.title = str(nvortex)+" vortices found (indicated diameter / pressure drop)" |
|---|
| 296 | myplot.xlabel = "x distance (km)" |
|---|
| 297 | myplot.ylabel = "y distance (km)" |
|---|
| 298 | if method > 0: |
|---|
| 299 | #if method == 1: |
|---|
| 300 | #myplot.field = ustm |
|---|
| 301 | myplot.field = psfc2d |
|---|
| 302 | #myplot.field = polypsfc2d |
|---|
| 303 | myplot.vmin = -2.*std |
|---|
| 304 | myplot.vmax = +2.*std |
|---|
| 305 | myplot.vmin = mean - 6.*std |
|---|
| 306 | myplot.vmax = mean + 6.*std |
|---|
| 307 | else: |
|---|
| 308 | myplot.field = anopsfc2d |
|---|
| 309 | myplot.vmin = -1.5 |
|---|
| 310 | myplot.vmax = 0.5 |
|---|
| 311 | myplot.fmt = "%.1f" |
|---|
| 312 | myplot.div = 20 |
|---|
| 313 | myplot.colorb = "spectral" |
|---|
| 314 | myplot.make() |
|---|
| 315 | |
|---|
| 316 | ### ANNOTATIONS |
|---|
| 317 | for iii in range(len(tabsize)): |
|---|
| 318 | ij = tabijcenter[iii] |
|---|
| 319 | coord1 = ij[1]*dx/1000. |
|---|
| 320 | coord2 = ij[0]*dx/1000. |
|---|
| 321 | txt = "%.0f/%.1f" % (tabsize[iii],tabdrop[iii]) |
|---|
| 322 | mpl.annotate(txt,xy=(coord1,coord2), |
|---|
| 323 | xytext=(-10,-30),textcoords='offset points',ha='center',va='bottom',\ |
|---|
| 324 | bbox=dict(boxstyle='round,pad=0.2', fc='yellow', alpha=0.3),\ |
|---|
| 325 | arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=-0.3',color='red'),\ |
|---|
| 326 | size='small') |
|---|
| 327 | |
|---|
| 328 | ###show detection |
|---|
| 329 | #lev = [-4,-2,-1,0,1,2,4] |
|---|
| 330 | #lev = [-1,0] |
|---|
| 331 | #mpl.contourf(myplot.absc,myplot.ordi,reslab,alpha=0.9,cmap=mpl.cm.get_cmap("binary_r"),levels=lev) |
|---|
| 332 | |
|---|
| 333 | ### SHOW OR SAVE IN FILE |
|---|
| 334 | mpl.show() |
|---|
| 335 | #save(mode="png",filename="detectm"+"_"+str(time)+"_"+str(method),folder="detect/",includedate=False) |
|---|
| 336 | |
|---|
| 337 | ## close data files |
|---|
| 338 | if save: |
|---|
| 339 | myfile1.close() |
|---|
| 340 | myfile2.close() |
|---|