# -*- coding: utf8 -*-
"""Utilities to deal with masks and selections
.. seealso::
Tutorials: :ref:`user.tut.misc.grid.masking`, :ref:`user.tut.misc.grid.polygons`
"""
# Copyright or © or Copr. Actimar/IFREMER (2010-2015)
#
# This software is a computer program whose purpose is to provide
# utilities for handling oceanographic and atmospheric data,
# with the ultimate goal of validating the MARS model from IFREMER.
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
#
#print 'importing masking 0'
from warnings import warn
import numpy as N
MA = N.ma
from cdms2.hgrid import TransientCurveGrid
from cdms2.grid import AbstractGrid
import cdms2, MV2
from mpl_toolkits.basemap import Basemap
from _geoslib import Point, Polygon, LineString
import gc
from genutil import minmax
cdms = cdms2
__all__ = ['get_coast', 'get_coastal_indices', 'GetLakes', 'polygon_mask', 'masked_polygon',
'polygons', 'd2m', 'polygon_select', 'envelop', 'check_poly_islands',
'check_poly_straits','t2uvmasks', 'mask2d', 'grid_envelop', 'convex_hull',
'uniq', 'rsamp', 'zcompress', 'Lakes', 'erode_coast', 'resol_mask',
'get_dist_to_coast', 'get_avail_rate', 'grid_envelop_mask', 'create_polygon',
'clip_shape', 'proj_shape', 'plot_polygon', 'clip_shapes', 'merge_masks']
__all__.sort()
[docs]def get_coast(mask, land=True, b=True, borders=True, corners=True):
"""Get a mask integer of ocean coastal points from a 2D mask
- **mask**: Input mask with 0 on water and 1 on land.
- *land*: If True, coast is on land [default: True]
- *corners*: If True, consider borders as coastal points [default: True]
- *borders:* If True, consider corners as coastal points [default: True]
At each point, return 0 if not coast, else an interger ranging from 1 to (2**8-1) to describe the coast point::
128 4 64
8 + 2
16 1 32
"""
# Initialize mask and its boundaries
sh = list(mask.shape)
imask = N.zeros((sh[0]+2, sh[1]+2), 'i')
imask[1:-1, 1:-1] = mask.astype('i')
imask[0] = imask[1]
imask[-1] = imask[-2]
imask[:, 0] = imask[:, 1]
imask[:, -1] = imask[:, -2]
for j in 0, -1:
jp = N.sign(j)
for i in 0, -1:
ip = N.sign(i)
imask[j, i] = imask[j+jp, j] + imask[j, i+ip]
# Coast on land or ocean
if not land:
iimask = imask
imask = 1-imask
else:
iimask = 1-imask
# Coast along borders only
mborders = \
1 *iimask[ :-2,1:-1]+ \
2 *iimask[1:-1,2: ]+ \
4 *iimask[2: ,1:-1]+ \
8 *iimask[1:-1, :-2]
# Coast as corners only
mcorners = \
16 *iimask[ :-2, :-2]+ \
32 *iimask[ :-2,2: ]+ \
64 *iimask[2: ,2: ]+ \
128*iimask[2: , :-2]
mcorners = N.where(N.equal(mborders, 0), mcorners, 0)
m = imask[1:-1,1:-1]*(borders*mborders+corners*mcorners)
del imask, iimask
gc.collect()
# Return
if b: return m.astype(bool)
return m
[docs]def get_coastal_indices(mask, coast=None, asiijj=False, **kwargs):
"""Get indices of ocean coastal points from a 2D mask
:Params:
- **mask**: Input mask with 0 on water and 1 on land.
- *boundary*: If True, consider outside boundary as land [default: True]
- *asiijj*: Get results as II,JJ instead of [(j0,i0),(j1,i1)...]
"""
if mask is MA.nomask: return []
if coast is None:
coast = get_coast(mask, **kwargs)
iy,ix = N.indices(mask.shape)
jj = N.compress(coast.ravel(),iy.ravel())
ii = N.compress(coast.ravel(),ix.ravel())
if asiijj: return ii, jj
return zip(jj, ii)
[docs]def get_dist_to_coast(grid, mask=None, proj=True):
"""Get the distance to coast on grid
:Params:
- **grid**: (x,y), grid or variable with a grid.
- **mask**: Land/sea mask or variable with a mask. If not provided,
gets mask from ``grid`` if it is a masked variable,
or estimates it using :func:`polygon_mask`
and a GHSSC shoreline resolution given by
:func:`~vacumm.misc.grid.basemap.gshhs_autores`.
- **proj**: Distance are computed by converting coordinates
from degrees to meters.
:Example:
>>> dist = get_dist_to_coast(sst.getGrid(), sst.mask)
>>> dist = get_dist_to_coast(sst)
:See also: :func:`get_coastal_indices`
"""
if mask is None and N.ma.isMA(grid):
mask = grid
xx, yy = M.get_xy(grid, proj=proj, mesh=True, num=True)
if mask is None:
from basemap import gshhs_autores, merc
x, y = M.get_xy(grid, proj=False, num=True, mesh=False)
res = gshhs_autores(x.min(), x.max(), y.min(), y.max())
mask = polygon_mask(grid, res)
elif N.ma.isMA(mask):
mask = N.ma.getmaskarray(mask)
mask = mask2d(mask)
ii, jj = get_coastal_indices(mask, asiijj=True)
good = ~mask
xc = xx[jj, ii]
yc = yy[jj, ii]
nc = len(xc)
ng = good.sum()
xdiff = N.resize(xx[good], (nc, ng)) - N.resize(xc, (ng, nc)).T
ydiff = N.resize(yy[good], (nc, ng)) - N.resize(yc, (ng, nc)).T
dists = xdiff**2+ydiff**2 ; del xdiff, ydiff
md = N.sqrt(dists.min(axis=0)) ; del dists
mindists = N.ma.zeros(xx.shape)
mindists[mask] = N.ma.masked
mindists[good] = md
del good
if cdms2.isVariable(grid):
mindists = MV2.asarray(mindists)
mindists.id = 'coastdist'
mindists.long_name = 'Distance to coast'
mindists.units = 'm'
M.set_grid(mindists, M.get_grid(grid))
return mindists
[docs]def erode_coast(var, mask2d=None, copy=True, maxiter=10, corners=0., hardmask=None, **kwargs):
"""Erode coastal mask of ``var`` to fit to ``mask2d`` using 2D smoothing
Soothing is performed using :func:`~vacumm.misc.filters.shapiro2d`,
in a convergence loop. Loop is ended if :
- the mask no longer changes,
- the number of iteration exceeds ``maxiter``.
.. warning::
Must be used only for erodeing a thin border
of the coast.
:Params:
- **var**: Atleast-2D A :mod:`MV2` variable with mask.
- **mask2d**, optional: Reference 2D mask.
- **maxiter**, optional: Maximal number of iteration for convergence loop.
If equal to ``None``, no check is performed.
- **corners**, optional: Weights of corners when calling :func:`~vacumm.misc.filters.shapiro2d`.
- **copy**, optional: Modify the variable in place or copy it.
- **hardmask**, optional: Mask always applied after an erosion step.
- Other keywords are passed to :func:`~vacumm.misc.filters.shapiro2d`.
:Return:
- A :mod:`MV2` variable
:Tutorial: :ref:`user.tut.misc.grid.masking.erode_coast`
"""
from vacumm.misc.filters import shapiro2d
varo = var.clone() if copy else var
if mask2d is None:
mask2d = N.zeros(var.shape[-2:], '?')
masknd = mask2d if (var.ndim==2) else N.resize(mask2d, var.shape)
if hardmask is not None and hardmask.ndim != var.ndim:
hardmask = N.resize(hardmask, var.shape)
# Not masked
if varo.mask is MV2.nomask or not var.mask.any():
varo[:] = MV2.masked_where(masknd, varo)
return varo
# Convergence loop
oldnmasked = varo.mask.sum()
i=0
while maxiter is None or i<maxiter:
varc = shapiro2d(varo, corners=corners, copy=True, mask='minimal', **kwargs)
varo[:] = MV2.where(varo.mask, varc, varo)
if hardmask is not None:
varo[:] = MV2.masked_where(hardmask, varo, copy=0)
del varc
maskc = varo.mask | masknd
nmasked = maskc.sum()
if nmasked == oldnmasked: break
del maskc
oldnmasked = nmasked
i+=1
varo[:] = MV2.masked_where(masknd, varo, copy=0)
return varo
[docs]class Lakes(object):
"""Find lakes in a 2D mask where 0 is water and 1 is land
:Example:
>>> from vacumm.misc.grid import Lakes
>>> import numpy as N
>>> from pylab import pcolor,show,title
>>> # Build the mask
>>> mask=N.ones((500,500))
>>> mask[3:10,100:102]=0
>>> mask[103:110,200:210]=0
>>> mask[203:210,200:220]=0
>>> # Find the lakes
>>> lakes = Lakes(mask,nmaxcells=80)
>>> print 'Number of lakes:', len(lakes.indices())
Number of lakes: 2
>>> pcolor(lakes.mask(),shading=False)
>>> title('Lakes')
>>> show()
>>> first_two_lakes = lakes[:2]
"""
def __init__(self,mask,nmaxcells=None):
# Inits
self._nmaxcells = nmaxcells
self._ny,self._nx = mask.shape
self._mask = N.array(mask[:], dtype='?')
self._imask = self._mask.astype('i')
self._iy, self._ix = N.indices(mask.shape)
self._np = self._imask.sum()
# Find lakes using labelling
import scipy.ndimage
self._lakes, self.nlakes = scipy.ndimage.label(1-self._imask)
self._ncells = [N.equal(self._lakes, ilake).astype('i').sum() for ilake in xrange(1, self.nlakes+1)]
# Compute indices
self._indices = []
for ilake in xrange(1, self.nlakes+1):
mask = self._lakes == ilake
self._indices.append(zip(self._ix[mask], self._iy[mask]))
# Sorting argument
self._argsort = N.argsort(self._ncells)[::-1]
def __call__(self,**kwargs):
return self.indices(**kwargs)
def __len__(self):
return self.nlakes
def __getitem__(self, item):
lakes = N.zeros(self._lakes.shape, 'l')
for lakeid in self._size2ids_(item):
lakes[self._lakes==lakeid] = lakeid
return lakes
def _size2ids_(self, item):
ids = N.arange(self.nlakes)[self._argsort[item]]
ids += 1
if isinstance(ids, int): ids = N.asarray([ids])
return ids
[docs] def plot(self, **kwargs):
"""Display the lakes
Keywords are passed to :func`~matplotlib.pyplot.pcolor`
"""
ny, nx = self._mask.shape
xx, yy = N.meshgrid(N.arange(nx+1), N.arange(ny+1))
import pylab as P
P.pcolor(xx, yy, self.lakes(), **kwargs)
P.show()
[docs] def ncells(self):
"""Number of cell point for each lake"""
return self._ncells
[docs] def indices(self, id=None, nbig=None):
"""Return a list of lake coordinates
- *id*: Select lake #<id>
- *nbig*: Consider the first ``nbig`` greatest lakes
"""
if id is not None:
return self._indices[id]
if nbig is None:
return self._indices
indices = []
for lakeid in self._size2ids_(slice(0, nbig)):
indices.append(self._indices[lakeid])
return indices
[docs] def lakes(self, id=None, nbig=None):
"""Return an array of same size as masks, but with points in lakes set to its lake id, and others to set to zero
- *id*: Select lake #<id>
- *nbig*: Consider the first ``nbig`` greatest lakes
"""
if id is None:
if nbig is not None:
return self[:nbig]
return self._lakes.copy()
return N.where(self._lakes==id, self._lakes, 0)
[docs] def mask(self, id=None, nbig=None, **kwargs):
"""Returns a boolean land/sea mask from lakes() (land is True)
- *id*: Select lake #<id>
- *nmaxcells*: Do not consider lakes with number of points greater than nmaxcells
:Example:
>>> mask_lake2 = GetLakes(mask).mask(2)
"""
return self.lakes(id=id, nbig=nbig) == 0
[docs] def ocean(self, mask=False):
"""Get the biggest lake or its mask (integer type)
- *mask*: If True, return the mask, not the lake [default: True]
"""
if mask:
return self.mask(nbig=1)
return self[0]
GetLakes = Lakes
[docs]def polygon_mask(gg, polys, mode='intersect', thresholds=[.5, .75],
ocean=False, fractions=0, yclean=True, premask=None, proj=False):
"""Create a mask on a regular or curvilinear grid according to a polygon list
:Params:
- **gg**: A cdms grid or variable or a tuple of (xx,yy).
- **polys**: A list of polygons or GMT resolutions or Shapes instance like shorelines.
- **mode**, optional: Way to decide if a grid point is masked. Possible values are:
- ``intersect``, 1, ``area`` (default): Masked if land area fraction is > ``thresholds[0]``.
If more than one intersections, land area fraction must be > ``thresholds[1]``
to prevent masking straits.
- else: Masked if grid point inside polygon.
- **thresholds**, optional: See ``intersect`` mode [default: [.5, .75]]
- **ocean**, optional: If True, keep only the ocean (= biggest lake).
- **fractions**: If True or 1, return the total fraction of cells covered by the
polygons; if 2, returns the total area for each cell.
- **proj**, optional: Geographical projection function.
See :func:`~vacumm.misc.grid.basemap.get_proj`. Use ``False`` to not
convert coordinates to meters, and speed up the routine.
:Result:
A :mod:`numpy` array of boolean (mask) with ``False`` on land.
"""
# Get proper numeric axes
gg = M.get_grid(gg)
if proj is not False:
proj = get_proj(gg)
curved = M.isgrid(gg, curv=True) or proj
xx, yy = M.get_xy(gg, mesh=True, num=True, proj=proj)
# Thresholds
if not isinstance(thresholds, (list, tuple)):
thresholds = [thresholds, thresholds]
elif len(thresholds) == 1:
thresholds += thresholds
# Bounds if mode is 'intersect'
bmode = mode in ['intersect', 1, 'area']
fmode = fractions #mode in [2, 'fractions', 3]
if fmode:
bmode = True
mask = N.zeros(xx.shape)
else:
mask = N.zeros(xx.shape, dtype='?')
xxb = M.bounds2d(xx)
yyb = M.bounds2d(yy)
if bmode:
# yyb[yyb>90.] = 89.99
# yyb[yyb<-90.] = -89.99
dx = xxb.ptp()/xxb.shape[1]
dy = yyb.ptp()/yyb.shape[0]
clip = (xxb.min()-dx, yyb.min()-dy, xxb.max()+dx, yyb.max()+dy)
ymins = yyb[:, 0, 0]-dy/10.
xmin = xxb[0, 0].min()-dx/10.
xmax = xxb[0, -1].max()+dx/10.
ymax = yyb[-1, 0].max()+dy/10.
else:
dx = xx.ptp()/xx.shape[1]
dy = yy.ptp()/yy.shape[0]
clip = (xx.min()-dx, yy.min()-dy, xx.max()+dx, yy.max()+dy)
ymins = yy[:, 0]-dy/10.
xmin = xx[0, 0]-dx/10.
xmax = xx[0, -1]+dx/10.
ymax = yy[-1, 0]+dy/10.
if curved:
xxc, yyc = M.meshcells(xx, yy)
dxx = N.diff(xxc, axis=1)
dxy = N.diff(xxc, axis=0)
dyx = N.diff(yyc, axis=1)
dyy = N.diff(yyc, axis=0)
for var in dxx, dxy, dyx, dyy:
var /= 10.
if premask is not None:
premask = N.asarray(premask, 'i')
# Get instances of Polygons
polys = polygons(polys, clip=clip, shapetype=2, proj=proj, clip_proj=False)
# Loop on grid points
skipped = 0
for j in xrange(xx.shape[0]):
# Get polygons of the curent row
ypolys = []
if curved:
# base
rowdown = N.vstack((xxc[j], yyc[j])).T
rowup = N.vstack((xxc[j+1], yyc[j+1])).T
# bottom margin
rowdown[1:-1, 0] -= .5*(dyx[j, :-1]+dyx[j, 1:])
rowdown[1:-1, 1] -= dyy[j, 1:-1]
# top margin
rowup[1:-1, 0] += .5*(dyx[j, :-1]+dyx[j, 1:])
rowup[1:-1, 1] += dyy[j, 1:-1]
# margin at corners
for ij, ddx, ddy in (0, dxx, dxy), (1, dyx, dyy): # x,y
rowdown[0, ij] -= ddx[j, 0] + ddy[j, 0]
rowdown[-1, ij] += ddx[j, -1] - ddy[j, -1]
rowup[0, ij] -= ddx[j+1, 0] - ddy[j, 0]
rowup[-1, ij] += ddx[j+1, -1] + ddy[j, -1]
rowpoly_array = N.vstack((rowdown, rowup[::-1]))
del rowdown, rowup
else:
rowpoly_array = N.array([
[xmin, yyb[j, 0, 0]-dy/10.], [xmax, yyb[j, 0, 0]-dy/10.],
[xmax, yyb[j, 0, 3]+dy/10.], [xmin, yyb[j, 0, 3]+dy/10.]])
rowpoly = Polygon(rowpoly_array)
for poly in polys:
try:
if poly.intersects(rowpoly):
ypolys.extend(poly.intersection(rowpoly))
except:
pass
del rowpoly_array, rowpoly
for i in xrange(xx.shape[1]):
# Pre-masking
if premask is not None and premask[j, i] != -1:
mask[j, i] = premask[j, i]
skipped += 1
continue
# Point is inside
if not bmode:
for poly in ypolys:
mask[j, i] = Point((xx[j, i],yy[j, i])).within(poly)
if mask[j, i]: break
continue
# Check the cell
cell_poly = Polygon(N.asarray([xxb[j, i, :], yyb[j, i, :]]).transpose())
if isinstance(cell_poly, (LineString, Point)):
raise TypeError, 'cell'
intersect_area = 0.
nintersect = 0
for ip, poly in enumerate(ypolys):
if poly is None: continue
if isinstance(poly, (LineString, Point)):
raise TypeError, 'poly'
# Check X range
if xxb[j, i, :].max() <= poly.boundary[:, 0].min() or \
xxb[j, i, :].min() >= poly.boundary[:, 0].max():
continue
# Check if polygon covers cell
if cell_poly.within(poly):
nintersect = 1
intersect_area = cell_poly.area()
break
# One polygon
ok = False
if cell_poly.intersects(poly):
# Remove from list of polys if inside cell
if poly.within(cell_poly):
intersect_area += poly.area()
nintersect += 1
del ypolys[ip]
continue
try:
intersections = cell_poly.intersection(poly)
except:
continue
for intersection in intersections:
if isinstance(intersection, (LineString, Point)):
continue
# One intersection polygon
this_area = intersection.area()
intersect_area += this_area
nintersect += 1
if this_area == poly.area():
ok = True
break
del intersections
if ok: break
# Fraction
land_fraction = intersect_area/cell_poly.area()
if fmode: # Fraction only
if fmode==2:
mask[j, i] = intersect_area
else:
mask[j, i] = land_fraction
continue
# Mask according to fraction (basic masking + strait checking)
mask[j, i] = land_fraction >= thresholds[nintersect > 1]
# mask[j, i] = intersect_fraction < thresholds[1] and nintersect > 1
del cell_poly
del ypolys
# Select ocean only
if ocean:
return GetLakes(mask).ocean()
return mask
[docs]def masked_polygon(vv, polys, copy=0, reverse=False, **kwargs):
"""Mask a variable according to a polygon list
:Params:
- **vv**: The MV2 array.
- **polys**, optional: Polygons (or something like that, see :func:`polygons`).
- **copy**, optional: Copy data?.
- **reverse**, optional: Reverse the mask?
- Other keywords are passed to :func:`polygon_mask`.
"""
# Get mask
mask = polygon_mask(vv, polys, **kwargs)
if reverse: mask = ~mask
# Mask the variable
vv2 = MV2.masked_where(mask, vv, copy=copy)
vv2.id = vv.id
del mask
return vv2
def masked_ocean(vv, polys=None, **kwargs):
"""Remove lakes from a variable to keep only ocean where ocean the the biggest lake
- **vv**: The variable
"""
pass
[docs]def proj_shape(shape, proj):
"""Project a Point, a Polygon or a LineString shape using a geographical projection
:Params:
- **shape**: A Point, Polygon, or a LineString instance with coordinates in degrees.
- **proj**: A projection function of instance.
See :func:`~vacumm.misc.grid.basemap.get_proj`.
:Return: A similar instance with its coordinates converted to meters
"""
if not callable(proj): return shape
# Point
if isinstance(shape, Point):
return Point(*proj(shape.boundary))
# LineString and Polygon
return shape.__class__(proj(*shape.boundary.T).T)
[docs]def clip_shape(shape, clip=None):
"""Clip a :class:`Point`, a :class:`Polygon` or a :class:`LineString`
shape using clipping polygon
:Params:
- **shape**: A valid :class:`Point`, a :class:`Polygon` or a
:class:`LineString` instance.
- **clip**, optional: A clipping polygon.
:Return: A possible empty list of intersection shapes.
"""
if clip is None: return [shape]
clip = create_polygon(clip)
shapes = []
if shape.within(clip):
return [shape]
if shape.intersects(clip):
try:
return shape.intersection(clip)
except:
pass
return shapes
[docs]def clip_shapes(shapes, clip=None):
"""Same as :func:`clip_shape` but applied to a list of shapes"""
clip = create_polygon(clip)
shapes = []
for shape in shapes:
shapes.extend(clip_shape(shape, clip))
return shapes
[docs]def create_polygon(data, proj=False, mode='poly'):
"""Create a simple :class:`Polygon` instance using data
:Param:
- **data**: Can be either a ``(N,2)`` or ``(2,N)`` array,
a ``(xmin,ymin,xmax,ymax)`` list, tuple or array, or a Polygon.
- **proj**, optional: Geographical projection function.
- **mode**, optional: Output mode. ``"line"`` = :class:`LineString`,
``"verts"`` = numpy vertices, else :class:`Polygon`.
"""
if isinstance(data, Polygon):
if callable(proj):
data = data.boundary
else:
return data
# Convert to numeric
data = N.asarray(data, 'float64')
# xmin,ymin,xmax,ymax form
if data.ndim == 1:
assert len(data) == 4, '1D form must have 4 elements (xmin,ymin,xmax,ymax), not %i'%len(data)
xmin,ymin,xmax,ymax = data
data = N.asarray([[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]])
# Check order
if data.shape[0] == 2:
data = data.T
# Projection
if callable(proj):
xdata, ydata = proj(*data.T)
data = N.asarray([xdata, ydata]).T
# Create Polygon or LineString
mode = str(mode)
if mode.startswith('v'): return data
shaper = LineString if mode.startswith('l') else Polygon
return shaper(data)
[docs]def plot_polygon(poly, ax=None, **kwargs):
"""Simply plot a polygon on the current plot using :func:`matplotlib.pyplot.plot`
:Params:
- **poly**: A valid :class:`Polygon` instance.
- Extra keyword are passed to plot function.
"""
xx = poly.boundary[:, 0]
yy = poly.boundary[:, 1]
xx = N.concatenate((xx, xx[:1]))
yy = N.concatenate((yy, yy[:1]))
if ax is None:
from matplotlib.pyplot import gca
ax = gca()
return ax.plot(xx, yy, **kwargs)
[docs]def polygons(polys, proj=None, clip=None, shapetype=2, **kwargs):
"""Return a list of Polygon instances
:Params:
- **polys**: A tuple or list of polygon proxies (see examples).
- **shapetype**: 1 = Polygons, 2=Polylines(=LineStrings) [default: 2]
- **proj**: Geographical projection to convert positions. No projection
by default.
- **clip**, optional: Clip all polygons with this one.
:Example:
>>> import numpy as N
>>> X = [0,1,1,0]
>>> Y = N.array([0,0,1,1])
>>> polygons( [(X,Y)]] ) # from like grid bounds
>>> polygons( [zip(X,Y), [X,Y], N.array([X,Y])] ) # cloud
>>> polygons( [polygons(([X,Y],), polygons(([X,Y],)]) # from polygins
>>> polygons( [[min(X),min(Y),max(X),max(Y)],] ) # from bounds
"""
# Input
if isinstance(polys, (Basemap, str, Polygon, N.ndarray, Shapes, AbstractGrid, tuple)):
polys = [polys]
if kwargs.has_key('m'): proj = kwargs['m']
kwclip = kwfilter(kwargs, 'clip_')
# Numeric or geos polygons
out_polys = []
# gmt_polys = []
if shapetype == 1:
shaper = LineString
else:
shaper = Polygon
if clip is not None:
clipl = clip # degrees
kwclip.setdefault('proj', proj)
clip = create_polygon(clip, **kwclip)
del kwclip['proj']
kwclip['mode'] = 'verts'
else:
clipl = None
# Loop on polygon data
from misc import isgrid, isrect, curv2rect
for poly in polys:
# Grid (cdms var, grid or tuple of axes) -> get envelop
if cdms2.isVariable(poly) or isgrid(poly) or \
(isinstance(poly, tuple) and len(poly)==2 and islon(poly[1]) and islat(poly[0])):
grid = M.get_grid(poly)
if grid is None: continue
poly = grid_envelop(grid)
# It's already a polygon
if isinstance(poly, Polygon):
pp = [poly]
# Polygon from GMT
if isinstance(poly, (str, Basemap)):
# poly = GSHHS_BM(poly)
# gmt_polys.append(poly)
out_polys.extend(GSHHS_BM(poly, clip=clipl, proj=proj).get_shapes())
continue
# Shapes instance
if isinstance(poly, Shapes):
# Good polygon?
if poly.get_type() == 0 or poly.get_type() != shapetype:
continue
# # Clip
# poly.clip(clip)
# Append to list
out_polys.extend(poly.get_shapes())
# continue
# Make sure to have a polygon with the right projection
poly = create_polygon(poly, proj=proj,
mode='line' if shaper is LineString else 'poly')
# Clip
if clip is not None:
out_polys.extend(clip_shape(poly, clip))
else:
out_polys.append(poly)
# # Treat GMT polygons
# from ...misc.plot import map as Map
# for poly in gmt_polys:
# if isinstance(poly, Basemap) and poly.resolution is not None:
# # We already have all the numeric polygons
# gmt_m = poly
# elif isinstance(poly, str):
# # We must deduce polygons from gmt resolution
# print 'getting gmt map for mask'
# assert poly[0] in ['f', 'h', 'i', 'l', 'c'], \
# "Resolution must be one of ['f', 'h', 'i', 'l', 'c'], not "+poly[0]
# # What kind of map?
# if m is not None:
# proj = m.projection
# else:
# proj = 'cyl'
# # Ok, get it
# gmt_m = Map(lon=lon, lat=lat, maponly=True, res=poly[0], projection=proj)
# else:
# continue
# out_polys.extend([Polygon(N.array(p).transpose()) for p in gmt_m.coastpolygons])
return out_polys
def _trans_(trans, xx, yy):
#FIXME: see deg2map
# Nothing to do
if trans is False: return xx, yy
# Check if we are sure to not have degrees
xx = N.asarray(xx)
yy = N.asarray(yy)
xmin = xx.min()
ymin = yy.min()
xmax = xx.max()
ymax = yy.max()
if xmin >= -0.1 and ymin >= -0.1 and (xmax > 720. or ymax > 90.):
return xx, yy
# We use a existing basemap instance
if callable(trans):
return trans(xx, yy)
# We create an instance
lat_center = yy.mean()
m = Basemap(lat_0=lat_center, lat_ts=lat_center, lon_0=xx.mean(), resolution=None,projection='merc',
llcrnrlon=xmin,llcrnrlat=ymin,urcrnrlon=xmax,urcrnrlat=ymax)
return m(xx, yy)
# return d2m(xx, yy)
[docs]def d2m(x, y):
return deg2m(x, y), deg2m(y)
[docs]def polygon_select(xx, yy, polys, zz=None, mask=False):
"""Select unstructered points that are inside polygons
:Params:
- **xx**: Positions along X.
- **yy**: Positions along Y.
- **polys**: Polygons.
- **zz**, optional: Values at theses positions.
- **mask**, optional: Return masked positions and values of True or 1,
:Examples:
>>> xs, ys = polygon_select(x, y, [poly1, poly2])
>>> xs, ys, zs = polygon_select(x, y, [poly1, poly2], z)
>>> xmasked, ymasked, zmasked = polygon_select(x, y, polys, z, mask=True)
>>> print N.allclose(xs, xmasked.compressed())
>>> valid = polygon_select(x, y, polys, mask=2)
>>> print N.allclose(xs, x[valid])
:Outputs: Depends on ``mask``
- False or 0: selected ``x,y`` or ``x,y,z``
- True/1: the same but as masked arrays
- 2: boolean array of valid points (the inverse of the mask)
"""
# Get numeric axes
xx = N.asarray(xx, 'd')
yy = N.asarray(yy, 'd')
# Get a proper polygon list
polys = polygons(polys, lon=(xx.min(), xx.max()), lat=(yy.min(), yy.max()))
# Create the mask
pmask = N.ones(xx.shape, '?')
for ip, (x, y) in enumerate(zip(xx, yy)):
for poly in polys:
if Point((x,y)).within(poly):
pmask[ip] = False
break
# Return mask or masked
if mask:
if mask==2:
return ~pmask
else:
return N.ma.masked_where(pmask, xx, copy=0), \
N.ma.masked_where(pmask, yy, copy=0), \
N.ma.masked_where(pmask, zz, copy=0)
# Return selection
good = ~pmask
del pmask
xsel = xx[good]
ysel = yy[good]
ret = (xsel, ysel)
if zz is not None:
ret += (zz[good], )
del good
return ret
[docs]def convex_hull(xy, poly=False, method='delaunay'):
"""Get the envelop of cloud of points
:Params:
- **xy**: (x,y) or array of size (2,nxy) (see :func:`~vacumm.misc.grid.misc.get_xy`).
- *poly*:
- ``True``: Return as Polygon instance.
- ``False``: Return two 1D arrays ``xpts,ypts``
- *method*:
- ``"angles"``: Recursive scan of angles between points.
- ``"delaunay"``: Use Delaunlay triangulation.
"""
# Points
xx, yy = M.get_xy(xy, proj=False)
xx = N.asarray(xx)
yy = N.asarray(yy)
if xx.ndim>1:
xx = xx.ravel()
yy = yy.ravel()
# Various methods
if method.startswith('delau'):
# Delaunay
from scipy.spatial import ConvexHull
xy = uniq(N.asarray([xx, yy]).transpose())
indices = ConvexHull(xy).vertices
xe = xy[indices, 0]
ye = xy[indices, 1]
elif method == 'quarters':
np = len(xx)
# Most south point
ip = N.argmin(yy)
xe = [xx[ip]]
ye = [yy[ip]]
# SW
while True:
good = xx>xe[-1]
if not good.any(): break
ip = N.argmin(yy[good])
xe.append(xx[ip])
ye.append(yy[ip])
# NW
while True:
good = yyx>ye[-1]
if not good.any(): break
ip = N.argmax(xx[good])
xe.append(xx[ip])
ye.append(yy[ip])
pass
#TODO: finish convex_hull with quaters
elif method=='angles':
# Angles
np = len(xx)
xx0 = N.resize(xx, (np, np))
xx1 = N.resize(xx, (np, np)).transpose()
dx = xx1-xx0 ; del xx0, xx1
yy0 = N.resize(yy, (np, np))
yy1 = N.resize(yy, (np, np)).transpose()
dy = yy1-yy0 ; del yy0, yy1
angles = N.arctan2(dx, dy) ; del dx, dy
idx = N.arange(np)
angles[idx, idx] = 10.
# Most south point
ip0 = ip = N.argmin(yy)
xe = [xx[ip]]
ye = [yy[ip]]
# Recursive search
ic = 0
while True:
ip = N.argmin(angles[ip])
if ip == ip0: break
xe.append(xx[ip])
ye.append(yy[ip])
ic += 1
if ic > np: break
xe = N.asarray(xe)
ye = N.asarray(ye)
else:
raise NotImplementedError
# Polygon or positions?
if poly:
return polygons([(xe, ye)], m=False)[0]
return xe, ye
[docs]def uniq(data):
"""Remove duplicates in data
:Example:
>>> import numpy as N
>>> a = N.arange(20.).reshape((10,2))
>>> a[5] = a[1]
>>> b = uniq(a)
"""
if data.ndim>2:
wdata = data.reshape((data.shape[0], data.size/data.shape[0]))
else:
wdata = data
keep = N.ones(len(data), '?')
for i, d in enumerate(data):
if not keep[i]: continue
bad = data == d
if data.ndim>1:
bad = bad.all(axis=-1)
bad[i] = False
keep = keep & ~bad
return data[keep]
[docs]def envelop(*args, **kwargs):
"""Shortcut to :func:`convex_hull`"""
return convex_hull(*args, **kwargs)
[docs]def grid_envelop(gg, centers=False, poly=True):
"""Return a polygon that encloses a grid
- **gg**: A cdms grid or a tuple of (lat,lon)
- *centers*:
- ``True``: The polygon is at the cell center.
- ``False``: The polygon is at the cell corners.
- *poly*:
- ``True``: Return as Polygon instance.
- ``False``: Return two 1D arrays ``xpts,ypts``
"""
# Axes
x, y = M.get_xy(gg, num=True)
# Rectangular grids
gg = M.curv2rect(gg,mode=False)
if M.isgrid(gg, curv=False):
if centers:
x0 = x.min() ; x1 = x.max()
y0 = y.min() ; y1 = y.max()
else:
xb = M.bounds1d(x)
yb = M.bounds1d(y)
x0 = xb.min() ; x1 = xb.max()
y0 = yb.min() ; y1 = yb.max()
if poly:
return Polygon(N.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]], 'd'))
return N.array([x0, x1]), N.array([y0, y1])
# Get the 2D axes
xx, yy = M.meshgrid(x, y)
xxb, yyb = M.meshbounds(x, y)
if centers:
xxref, yyref = xx, yy
else:
xxref, yyref = xxb, yyb
ny, nx = xx.shape
# Get the limits
xp = []
yp = []
for jslice, islice in (
(0, slice(None)), # bottom
(slice(1, -1), -1), # inner-left
(-1, slice(None, None, -1)), # top
(slice(-2, 0, -1), 0)): # inner right
# Border
xborder = xxref[jslice, islice].tolist()
yborder = yyref[jslice, islice].tolist()
# Smart compression
if isinstance(jslice, int): # Along X
if jslice: # top
jb0 = -2 ; jb1 = None
else: # bottom
jb0 = 0 ; jb1 = 2
xpoly = xxb[jb0:jb1, islice][:, ::nx]
ypoly = yyb[jb0:jb1, islice][:, ::nx]
else: # Along Y
if islice: # right
ib0 = -2 ; ib1 = None
else: # left
ib0 = 0 ; ib1 = 2
xpoly = xxb[jslice, ib0:ib1][::ny-2]
ypoly = yyb[jslice, ib0:ib1][::ny-2]
xline = xx[jslice, islice] ; xline = xline[::len(xline)-1]
yline = yy[jslice, islice] ; yline = yline[::len(yline)-1]
poly1d = Polygon(N.asarray([[xpoly[0, 0], ypoly[0, 0]],
[xpoly[0, -1], ypoly[0, -1]], [xpoly[-1, -1], ypoly[-1, -1]],
[xpoly[-1, 0], ypoly[-1, 0]]], 'd'))
line1d = LineString(N.asarray([[xline[0], yline[0]], [xline[1], yline[1]]], 'd'))
if line1d.within(poly1d): # Compression
xborder = xborder[::len(xborder)-1]
yborder = yborder[::len(yborder)-1]
# Add it
xp += xborder
yp += yborder
# Return the polygon or arrays
if not poly: return N.asarray(xp), N.asarray(yp)
return Polygon(N.asarray(zip(xp, yp)))
[docs]def grid_envelop_mask(ggi, ggo, poly=True, **kwargs):
"""Create a mask on output grid from the bounds of an input grid:
points of output grid that are not within bounds of input grid are masked.
:Params:
- **ggi**: Input grid or (lon,lat) or cdms variable.
- **ggo**: Output grid or (lon,lat) or cdms variable.
- Other keywords are passed to :func:`grid_envelop`
:Returns:
A mask on output grid, where True means "outside bounds of input grid".
"""
if M.isgrid(ggi, curv=False): # Rectangular
x, y = M.get_xy(ggi)
xb = M.bounds1d(x)
yb = M.bounds1d(y)
ggo = M.get_grid(ggo)
xxo, yyo = M.meshgrid(*M.get_xy(ggo))
return (xxo<=xb.min())|(xxo>=xb.max())|(yyo<=yb.min())|(yyo>=yb.max())
elif poly: # Using a polygon
# Envelop of the input grid
poly = grid_envelop(ggi, **kwargs)
# Mask on output grid
return ~polygon_mask(ggo, [poly], mode='inside')
else: # Using regridding
from regridding import regrid2d
xxb, yyb = M.meshbounds(*M.meshbounds(*M.get_xy(ggi)))
vari = MV2.asarray(xxb*0.)
vari[[0, -1]] = 1.
vari[:, [0, -1]] = 1.
M.set_grid(vari, M.curv_grid(xxb, yyb))
varo = regrid2d(vari, ggo, method='nearest', ext=True).filled(1.)
return varo.astype('?')
[docs]def check_poly_islands(mask, polys, offsetmin=.85, offsetmax=1.5, dcell=2):
"""Check that islands as greater as a cell are taken into account
- **mask**: The initial mask. A cdms variable with X (longitude) and Y (latitude), or a tuple (lon, lat, mask).
- **polys**: Coastal polygons.
- *offset*: Islands whose area is > 1-offset and < 1+offset % of the mean cell area are checked [default: .95]
- *dcell*: dx and dy relative extension from the center of the cell in resolution units.
"""
# Get mask and axes
if isinstance(mask, tuple):
if len(mask) == 3:
xx, yy, mask = mask
else:
gg, mask = mask
xx, yy = M.get_xy(gg)
else:
xx, yy = M.get_xy(mask)
# Resolution
dx = N.diff(xx).mean()
dy = N.diff(yy).mean()
cell_area = N.sqrt(dx*dy)
# Loop on islands
ni = 0
for island_poly in polygons(polys):
# Select islands
island_area = island_poly.area()
if island_area < cell_area*offsetmin or island_area > cell_area*offsetmax: continue
# Define cells around
xisland = island_poly.get_data()[:, 0]
yisland = island_poly.get_data()[:, 1]
xcisland = (xisland.min()+xisland.max())/2.
ycisland = (yisland.min()+yisland.max())/2.
imin, imax, k = xx.mapInterval((xcisland-dx*dcell*1.1, xcisland+dx*dcell*1.1))
jmin, jmax, k = xx.mapInterval((ycisland-dy*dcell*1.1, ycisland+dy*dcell*1.1))
amask = mask[jmin:jmax, imin:imax]
xxb, yyb = _trans_(trans, bounds2d(amask.getAxis(-1)), bounds2d(amask.getAxis(-2)))
# Get intersection areas
areas = N.zeros(amask.shape, 'f')
for j in xrange(amask.shape[-2]):
for i in xrange(amask.shape[-1]):
cell_poly = Polygon(N.array([xxb[j, i, :].ravel(), yyb[j, i, :].ravel()]).transpose())
if cell_poly.intersects(island_poly):
areas[j, i] = cell_poly.intersection(island_poly).area()
# It should be masked where area is max
if not amask.flat[N.argmax(areas)]: ni += 1
amask.flat[N.argmax(areas)] = True
mask[jmin:jmax, imin:imax] = amask
print 'Masked %i islands' % ni
return mask
[docs]def check_poly_straits(mask, polys, dcell=2, threshold=.75):
"""Check that straits are opened by counting the number of polygon intersection in each cell and the area of water
- **mask**: The initial mask. A cdms variable with X (longitude) and Y (latitude).
- **polys**: Coastal polygons.
- *dcell*: dx and dy relative extension from the center of the cell in resolution units.
- *threshold*: Maximal fraction of cell area that must be covered of land (> .5) [default: .25]
"""
# Get mask and axes
if isinstance(mask, tuple):
if len(mask) == 3:
xx, yy, mask = mask
else:
gg, mask = mask
xx, yy = M.get_xy(gg)
else:
xx, yy = M.get_xy(mask)
# Get cell bounds
xxb, yyb = bounds2d(xx), bounds2d(yy)
# Loop on coastal points
ns = 0
for i, j in get_coastal_indices(mask):
# Cell polygon
cell_poly = Polygon(N.array(xxb[j, i, :], yyb[j, i, :]).transpose())
cell_area = cell_poly.area()
# Count polygon intersections
npoly = 0
area = 0.
for poly in polys:
if poly.intersects(cell_poly):
intersections = poly.intersection(cell_poly)
npoly += len(intersections)
for intersection in intersections:
area += intersection.area()
if area > threshold: break
else:
continue
break
else:
# Several polygons = strait, so open
mask[j, i] = npoly < 2
print 'Opened %i straits' %ns
return mask
[docs]def t2uvmasks(tmask, getu=True, getv=True):
"""Compoute masks at U and V points from a mask at T points on a C grid (True is land)
- **tmask**: A 2D mask.
- *getu*: Get the mask at U-points
- *getv*: Get the mask at V-points
Return umask,vmask OR umask OR vmask depending on getu and getv.
"""
if not getu and not getv: return
if getu:
umask = N.array(tmask) # copy
umask[..., :, :-1] = tmask[..., :, :-1] | tmask[..., :, 1:]
if not getv: return umask
vmask = N.array(tmask)
vmask[..., :-1, :] = tmask[..., :-1, :] | tmask[..., 1:, :]
if not getu: return vmask
return umask, vmask
[docs]def mask2d(mask, method='and'):
"""Convert a 3(or more)-D mask to 2D by performing compression on first axes
.. note::
Mask is False on ocean
- **mask**: At least 2D mask
- *method*: Compression method
- ``"and"``: Only one point must be unmask to get the compressed dimension unmasked.
- ``"or"``: All points must be unmask to get the compressed dimension unmasked.
"""
assert mask.ndim > 1, 'Mask must be at leat 2D'
if mask.ndim == 2: return mask
dtype = mask.dtype
if method.lower() == 'or':
func = N.logical_or
else:
func = N.logical_and
while mask.ndim != 2:
mask = func.reduce(mask)
return mask.astype(dtype)
def _old_rsamp_(x, y, r, z=None, rmean=.7, proj=False, getmask=False):
"""Undersample data points using a radius of proximity
- **x**: 1D x array.
- **y**: 1D Y array.
- **r**: Radius of proximity.
- *z*: 1D Z array.
- *proj*: Geographic projection instance to convert coordinates.
- *method*:
- ``mean``: Average neighbouring points.
- ``pickup``: Pickup only the current point within the circle of proximity.
"""
# Need numpy arrays
x = N.asarray(x)
y = N.asarray(y)
if getmask:
z = None
if z is not None:
z = N.array(z)
n = x.shape[0]
dst = x.copy()
dst[:] = 0.
close = N.zeros(n, '?')
good = N.ones(n, '?')
rmean = N.clip(rmean, 0., 1.)
# Projection
if proj is True:
proj = get_proj((x,y))
if callable(proj):
x, y = proj(x, y)
# Loop on valid points
for i0 in xrange(n):
if not good[i0]: continue
# Search for neighbours
dst[:] = N.sqrt((x-x[i0])**2+(y-y[i0])**2)
close[:] = dst < r
# Average over neighbours
if z is not None and rmean>0.:
zclose = z[close]
z[i0] = zclose[dst[close] < r*rmean].mean()
del zclose
# Neighbours no longer valid
good &= ~close
good[i0] = True
del dst, close
# Only the mask
if getmask:
return good
# Select valid points
ret = (x[good], y[good])
if z is not None:
ret += z[good],
del good
return ret
[docs]def rsamp(x, y, r, z=None, rmean=.7, proj=False, rblock=3, getmask=False):
"""Undersample data points using a radius of proximity
- **x**: 1D x array.
- **y**: 1D Y array.
- **r**: Radius of proximity.
- *z*: 1D Z array.
- *proj*: Geographic projection instance to convert coordinates.
- *rmean*: Radius of averaging relative to ``r`` (``0<rmean<1``)
- *rblock*: Size of blocks relative to ``r`` for computation by blocks.
"""
# Need numpy arrays
x = N.asarray(x)
y = N.asarray(y)
if getmask:
z = None
if z is not None:
z = N.array(z)
n = x.shape[0]
good = N.ones(n, '?')
rmean = N.clip(rmean, 0., 1.)
rblock = int(max(1, rblock))
# Projection
if proj is True:
proj = get_proj((x,y))
if callable(proj):
x, y = proj(x, y)
# Setup blocks
xmin = x.min()
ymin = y.min()
xmax = x.max()
ymax = y.max()
dx = xmax-xmin ; dy = ymax-ymin
xmin -= .001*dx ; xmax += .001*dx
ymin -= .001*dy ; ymax += .001*dy
nxb = max(1, int((xmax-xmin)/(r*(rblock-1))))
nyb = max(1, int((ymax-ymin)/(r*(rblock-1))))
rblock *= r
xgood = N.zeros(n, '?')
block = N.zeros(n, '?')
# Loop on x bands
x0 = xmin+r-rblock
for ixb in xrange(nxb):
x0 += rblock-r
if ixb == nxb-1:
x1 = xmax
else:
x1 = x0 + rblock
xgood[:] = (x>=x0)&(x<=x1)
if not xgood.any(): continue
# Loop on y bands
y0 = ymin+r-rblock
for iyb in xrange(nyb):
y0 += rblock-r
if iyb == nyb-1:
y1 = ymax
else:
y1 = y0 + rblock
# Select the block
block[:] = xgood&(y>=y0)&(y<=y1)
xx = x[block]
nn = len(xx)
if not nn: continue
yy = y[block]
if z is not None:
zz = z[block]
gg = good[block]
# Loop on valid points
for i0 in xrange(nn):
if not gg[i0]: continue
# Search for neighbours
dst = N.sqrt((xx-xx[i0])**2+(yy-yy[i0])**2)
close = dst < r
# Average over neighbours
if z is not None and rmean>0.:
zclose = zz[close]
zz[i0] = zclose[dst[close] < r*rmean].mean()
del zclose
# Neighbours no longer valid
gg &= ~close
gg[i0] = True
# del
del dst, close
# Store results
good[block] = gg
if z is not None:
z[block] = zz
del zz
del xx, yy, gg
# Only the mask
if getmask:
return good
# Select valid points
ret = (x[good], y[good])
if proj:
ret = proj(ret[0], ret[1], inverse=True)
if z is not None:
ret += z[good],
del good
return ret
[docs]def zcompress(z, *xy, **kwargs):
"""Compress 1D arrays according to the mask of the first one
- **z**: Reference (masked) array
- *xy*: Additional arrays to be compressed
- *numpify*: Force convertion to numpy array
:Example:
>>> z, x, y = zcompress(z, x, y, numpify=True)
"""
if hasattr(z, 'mask') and z.mask is not N.ma.nomask:
good = ~z.mask
ret = ()
npf = kwargs.get('numpify', False)
for var in (z, )+xy:
# Compress
if not cdms2.isVariable(var):
newvar = var[good]
else:
newvar = cdms2.createVariable(var.asma()[good])
cp_atts(var, newvar, id=True)
# Numpify
if npf and hasattr(var, 'filled'):
ret += newvar.filled(),
del newvar
else:
ret += newvar,
return ret
ret = z,
ret += xy
return ret
[docs]def resol_mask(grid, res=None, xres=None, yres=None, xrelres=None, yrelres=None,
relres=None, scaler=None, compact=False, relmargin=.05, nauto=20):
"""Create a mask based on resolution criteria for undersampling 2D data
:Params:
- **grid**: A cdms array with a grid, a cdms grid or a tuple of axes.
- **res**, optional: Horizontal resolution of arrows
(in both directions) for undersampling [default: ``None``].
If ``'auto'``, resolution is computed so as to have at max ``nauto``
arrow in along an axis. If it is a :class:`complex` type, its imaginary part
set the ``nauto`` parameter and ``res`` is set to ``'auto'``.
.. warning::
Use of ``res`` makes the supposition that X and y units are consistent.
- **xres**, optional: Same along X [default: ``res``]
- **yres**, optional: Same along Y [default: ``res``]
- **relres**, optional: Relative resolution
(in both directions).
- If > 0, = ``median(res)*relres``.
- If < -1, =``min(res)*abs(relres)``.
- If < 0 and > -1, =max(res)*abs(relres)
- **xrelres**, optional: Same along X [default: ``relres``]
- **yrelres**, optional: Same along Y [default: ``relres``]
- **scaler**, optional: A callable object that transform X and Y coordinates.
Transformed coordinates are used instead of original coordinates
if resolutions are negatives.
A typical scaler is for example a geographic projection that convert degrees
to meters (like a :class:`~mpl_toolkits.basemap.Basemap` instance).
- **compact**, optional: If no unsersampling is efective, returns ``False``.
.. note:: A resolution value set to ``False`` implies no undersampling.
:Example:
>>> mask = resol_mask((x2d, y2d), relres=2.5) # sampling=2.5
>>> mask = resol_mask(var.getGrid(), xres=2., scaler=mymap, yres=-100.) # 100m
>>> mask = resol_mask(var.getGrid(), res=20j) # at max 20 arrows per axis
>>> mask = resol_mask(var.getGrid(), res='auto', nauto=20) # equivalent
.. note::
It is usually better to regrid with a convenient method
instead of unsersample because of aliasing.
"""
# Params
if isinstance(res, complex):
if int(res.imag)>1:
nauto = int(res.imag)
res = 'auto'
else:
res = None
if nauto is None: nauto = 20
# Get numeric axes
xx, yy = M.get_xy(grid, num=True, mesh=True, m=False)
# Scaler
if scaler is None:
scaler = get_proj((xx, yy))
if callable(scaler):
xxs, yys = scaler(xx, yy)
else:
xxs, yys = xx, yy
# Guess reference resolution
if res is not None:
if xres is None: xres = res
if yres is None: yres = res
if relres is not None:
if xrelres is None: xrelres = relres
if yrelres is None: yrelres = relres
if xres is False or xx.shape[1]<3: xres = None
if yres is False or yy.shape[1]<3: yres = None
if xrelres is False: xrelres = None
if yrelres is False: yrelres = None
if (xrelres is not None and xres is None) or (yrelres is not None and yres is None):
xmode = 'median'
ymode = 'median'
if xrelres is not None:
if xrelres < -1:
xmode = 'min'
elif xrelres < 0:
xmode = 'max'
if yrelres is not None:
if yrelres < -1:
ymode = 'min'
elif yrelres < 0:
ymode = 'max'
xr = M.resol(xx, axis=1, mode=xmode) if xres is None else xres
yr = M.resol(yy, axis=0, mode=ymode) if yres is None else yres
if N.abs(N.log10(xres/yres))<2: # similar coordinates
xr, yr = M.resol(xxs, yys, mode=(xmode, ymode))
if xres is None: xres = -xr
if yres is None: yres = -yr
else:
if xres is None: xres = xr
if yres is None: yres = yr
x2d = y2d = xbres = ybres = xcres = ycres = None
if xres=='auto' or yres=='auto':
x2d = xxs
y2d = yys
else:
x2d = xx if xres > 0 else xxs
y2d = yy if yres > 0 else yys
if xres is not None or yres is not None:
xbres, ybres = M.resol((x2d, y2d), mode='raw')
if xres=='auto' or yres=='auto':
xcres = N.ma.cumsum(xbres, axis=1)
ycres = N.ma.cumsum(ybres, axis=0)
xares = -1.*xcres.max()/nauto
yares = -1.*ycres.max()/nauto
xres = yres = min(xares, yares)
# else:
# x2d = y2d = xbres = ybres = xcres = ycres = None
# Resolution along X
if xres=='auto':
xres = res
if xres is not None: # From absolute resolution
if xrelres is None: xrelres = 1.
xbres /= N.abs(xres*N.abs(xrelres))
if xcres is None:
xcres = N.ma.cumsum(xbres, axis=1).astype('i')
else:
xcres /= N.abs(xres*N.abs(xrelres))
xcres = xcres.astype('i')
xdres = N.ma.diff(xcres, axis=1)
xmask = N.ones(xx.shape, '?')
xmask[:, 0] = False
xmask[:, 1] = (xcres[:, 0]==0).filled(True)
xmask[:, 2:] = (xdres==0).filled(True)
xmask[:, 1:] &= (xbres<=1-relmargin)
del xcres, xdres, xbres
else:
xmask = False
# Resolution along Y
if yres=='auto':
yres = res
if yres is not None: # From absolute resolution
if yrelres is None: yrelres = 1.
ybres /= N.abs(yres*N.abs(yrelres))
if ycres is None:
ycres = N.ma.cumsum(ybres, axis=0).astype('i')
else:
ycres /= N.abs(yres*N.abs(yrelres))
ycres = ycres.astype('i')
ydres = N.ma.diff(ycres, axis=0)
ymask = N.ones(yy.shape, '?')
ymask[0] = False
ymask[1] = (ycres[0]==0).filled(True)
ymask[2:] = (ydres==0).filled(True)
ymask[1:] &= (ybres<=1-relmargin)
del ycres, ydres, ybres
else:
ymask = False
return xmask | ymask
[docs]def get_avail_rate(data, num=True, mode='rate'):
"""Get the availability rate of a masked array along its forst dimension
:Params:
- **data**: Mask or masked array.
- **num**, optional: Get result as pure numpy array or formatted
as input array when possible (MV2 array).
- **mode**, optional:
- "rate": Output is between 0 and 1.
- "pct": Same but in percents from 0 to 100.
- "count": Count valid data.
"""
# Get the mask
mask = N.ma.getmaskarray(data) if N.ma.isMA(data) else data
# Count
rate = mask.sum(axis=0)
if mode=="rate" or mode=='pct':
rate = rate.astype('f')/rate.shape[0]
if mode=='pct':
rate *= 100
# Format?
if not num and cdms2.isVariable(data):
rate = MV2.array(rate, copy=0)
if data.getGrid() is not None:
M.set_grid(rate, data.getGrid())
if mode=="rate":
rate.long_name = 'Availability rate'
elif mode=='pct':
rate.long_name = 'Availability rate'
rate.units = "%"
else:
rate.long_name = 'Number of valid data'
return rate
[docs]def merge_masks(varlist, copy=False, mode='max'):
"""Merge the mask of a list of variable"""
if isinstance(varlist, N.ndarray):
if copy:
varlist = varlist.copy()
return varlist
varlist = list(varlist)
shape = varlist[0].shape
mask = N.ma.getmaskarray(varlist[0])
opname = '__or__' if mode=='max' else '__and__'
for var in varlist[1:]:
assert var.shape==shape, 'All arrays must have the same shape'
thismask = N.ma.getmaskarray(var)
mask = getattr(mask, opname)(thismask)
out = []
for var in varlist:
if not N.ma.isMA(var) and not copy:
warn('Not a masked array, thus cannot apply a mask without copy')
out.append(var)
continue
if copy:
if cdms2.isVariable(var):
var = var.clone()
else:
var = N.ma.array(var, copy=True)
var[:] = N.ma.masked_where(mask, var, copy=False)
out.append(var)
return out
######################################################################
######################################################################
import misc as M
from ...misc.axes import islon, islat
from ...misc.phys.units import deg2m
from ...misc.io import Shapes
from .basemap import GSHHS_BM, get_proj
from ...misc.misc import cp_atts, kwfilter