source: codes/icosagcm/devel/Python/dynamico/meshes.py

Last change on this file was 987, checked in by dubos, 5 years ago

devel/Python : ensure positive longitudes when plotting

File size: 41.8 KB
Line 
1from __future__ import absolute_import     
2
3import time
4import math
5import numpy as np
6import netCDF4 as cdf
7import matplotlib.tri as tri
8import matplotlib.pyplot as plt
9from matplotlib.patches import Polygon
10from matplotlib.collections import PatchCollection
11
12from dynamico import parallel
13from dynamico.util import list_stencil, BaseClass, inverse_list
14from dynamico import getargs
15from dynamico.partition import partition_mesh
16
17log_master, log_world = getargs.getLogger(__name__)
18INFO, DEBUG, ERROR = log_master.info, log_master.debug, log_world.error
19INFO_ALL, DEBUG_ALL = log_world.info, log_world.debug
20
21radian=180/math.pi # convert from radians to degrees
22def Triangulation(lon,lat): return tri.Triangulation((lon*radian+360.)%360., radian*lat)
23
24#------------------- Hybrid mass-based coordinate -------------
25
26# compute hybrid coefs from distribution of mass
27
28def compute_hybrid_coefs(mass):
29    if mass.ndim==2 :
30        nx,llm=mass.shape
31        mass_dak = np.zeros((nx,llm))
32        mass_dbk = np.zeros((nx,llm))
33        mass_bl = np.zeros((nx,llm+1))+1.
34        for i in range(nx):
35            m_i = mass[i,:]
36            dbk_i = m_i/sum(m_i)
37            mass_dbk[i,:] = dbk_i
38            mass_bl[i,1:]= 1.-dbk_i.cumsum()
39    if mass.ndim==3 :
40        nx,ny,llm=mass.shape
41        mass_dak = np.zeros((nx,ny,llm))
42        mass_dbk = np.zeros((nx,ny,llm))
43        mass_bl = np.zeros((nx,ny,llm+1))+1.
44        for i in range(nx):
45            for j in range(ny):
46                m_ij = mass[i,j,:]
47                dbk_ij = m_ij/sum(m_ij)
48                mass_dbk[i,j,:] = dbk_ij
49                mass_bl[i,j,1:]= 1.-dbk_ij.cumsum()
50 
51    return mass_bl, mass_dak, mass_dbk
52
53# from theta.k90       : rhodz(l,ij) = mass_dak(l,ij) + mass_col(ij)*mass_dbk(l,ij)
54# from caldyn_wflux.k90: wflux(l,ij) = mass_bl(l,ij) * dmass_col(ij) - convm(l,ij)
55def mass_coefs_from_pressure_coefs(g, ap_il, bp_il):
56    # p = Mg + ptop = a + b.ps
57    # ps = Mcol.g+ptop
58    # M = mass_a + mass_b.Mcol
59    # M = (a+b.ps-ptop)/g
60    #   = (a+b.ptop-ptop)/g + b.Mcol
61    # mass_a  = (a+b.ptop)/g
62    # mass_da = (da+db.ptop)/g
63    # mass_b  = b
64    # mass_db = db
65    n, llm, ptop = ap_il.shape[0], ap_il.shape[1]-1, ap_il[0,-1]
66    DEBUG('hybrid ptop : %f' % ptop) 
67    mass_al = (ap_il+ptop*bp_il)/g
68    mass_dak, mass_dbk = np.zeros((n,llm)), np.zeros((n,llm))
69    for k in range(llm):
70        mass_dak[:,k] = mass_al[:,k]-mass_al[:,k+1]
71        mass_dbk[:,k] = bp_il[:,k]-bp_il[:,k+1]
72    DEBUG('%s'%mass_al[0,:])
73    DEBUG('%s'%mass_dak[0,:])
74    DEBUG('%s'%mass_dbk[0,:])
75    return [ np.asarray(x) for x in bp_il, mass_dak, mass_dbk ]
76
77#----------------------- Base class for "user" meshes ---------
78
79class BaseMesh(BaseClass):
80    def zeros(self,dims): return np.zeros([n for n in dims if n>1]) # overriden by meshes.dev.DevMesh
81
82#----------------------- Cartesian mesh -----------------------
83
84# arrays is a list of arrays
85# vals is a list of tuples
86# each tuple is stored in each array
87def put(ij, deg, arrays, vals):
88    k=0
89    for vv in vals: # vv is a tuple of values to be stored in arrays
90        for array,v in zip(arrays,vv):
91            array[ij,k]=v
92        k=k+1       
93    deg[ij]=k
94
95def concat(x,y):
96    z = np.asarray([x,y])
97    return z.transpose()
98
99class Cartesian_Mesh(BaseMesh):
100    def __init__(self,nx,ny,llm,nqdyn,Lx,Ly,f):
101        dx,dy = Lx/nx, Ly/ny
102        self.dx, self.dy, self.f = dx,dy,f
103        self.nx, self.ny, self.llm, self.nqdyn = nx,ny,llm,nqdyn
104        self.field_z = self.field_mass
105        # 1D coordinate arrays
106        x=(np.arange(nx)-nx/2.)*Lx/nx
107        y=(np.arange(ny)-ny/2.)*Ly/ny
108        lev=np.arange(llm)
109        levp1=np.arange(llm+1)
110        self.x, self.y, self.lev, self.levp1 = x,y,lev,levp1
111        # 3D coordinate arrays
112        self.yy,self.xx,self.ll = np.meshgrid(y,x,lev, indexing='ij')       
113        self.yyp1,self.xxp1,self.llp1 = np.meshgrid(y,x,levp1, indexing='ij')
114        # beware conventions for indexing
115        # Fortran order : llm,nx*ny,nqdyn  / indices start at 1
116        # Python order : nqdyn,ny,nx,llm   / indices start at 0
117        # indices below follow Fortran while x,y follow Python/C
118        def periodize(z,nz): return (z+2*nz)%nz
119        def index(x,y):      return 1+periodize(x,nx)+nx*periodize(y,ny)
120        def indexu(x,y):     return 2*index(x,y)-1
121        def indexv(x,y):     return 2*index(x,y)
122        def zeros(shape,n=1): return [np.zeros(shape) for i in range(n)] if n>1 else np.zeros(shape)
123        def indices(shape,n=1): return [np.zeros(shape,np.int32) for i in range(n)] if n>1 else np.zeros(shape,np.int32)
124
125        primal_num, dual_num, edge_num = nx*ny, nx*ny, 2*nx*ny
126
127        Aiv, lon_i,lon_v,lat_i,lat_v   = zeros( nx*ny,   5)
128        lon_e,lat_e,le,de,angle_e      = zeros( 2*nx*ny, 5)
129        Riv2                           = zeros((nx*ny,4)  )
130        wee                            = zeros((2*nx*ny,4))
131        primal_bounds_lon, primal_bounds_lat, dual_bounds_lon, dual_bounds_lat = zeros( (nx*ny,4), 4)
132
133        primal_deg,dual_deg                 = indices( nx*ny,   2)
134        primal_edge,primal_ne,primal_vertex = indices((nx*ny,4),3)
135        dual_edge,dual_ne,dual_vertex       = indices((nx*ny,4),3)
136        trisk_deg,left,right,up,down        = indices( 2*nx*ny, 5)
137        trisk                               = indices((2*nx*ny,4)) # 4 TRiSK coefs per edge
138   
139        def putval(ij, arrays, vals): 
140            for array,v in zip(arrays,vals): array[ij]=v
141        for x in range(nx):
142            for y in range(ny):
143                # NB : Fortran indices start at 1
144                # primal cells
145                ij=index(x,y)-1
146                lon_i[ij], lat_i[ij] = x, y
147                put(ij, primal_deg,(primal_edge,primal_vertex,primal_ne, primal_bounds_lon, primal_bounds_lat),
148                    ((indexu(x,y),   index(x,y),      1, x+.5, y+.5), 
149                     (indexv(x,y),   index(x-1,y),    1, x-.5, y+.5),
150                     (indexu(x-1,y), index(x-1,y-1), -1, x-.5, y-.5),
151                     (indexv(x,y-1), index(x,y-1),   -1, x+.5, y-.5) ))
152                # dual cells
153                ij=index(x,y)-1
154                lon_v[ij], lat_v[ij] = x+.5, y+.5
155                put(ij,dual_deg,(dual_edge,dual_vertex,dual_ne,Riv2, dual_bounds_lon, dual_bounds_lat),
156                    ((indexv(x+1,y),index(x,y),      1, .25, x,    y),
157                     (indexu(x,y+1),index(x+1,y),   -1, .25, x+1., y),
158                     (indexv(x,y),  index(x+1,y+1), -1, .25, x+1., y+1.),
159                     (indexu(x,y),  index(x,y+1),    1, .25, x,    y+1.) ))
160                # edges :
161                # left and right are adjacent primal cells
162                # flux is positive when going from left to right
163                # up and down are adjacent dual cells (vertices)
164                # circulation is positive when going from down to up
165                # u-points
166                ij =indexu(x,y)-1 
167                putval(ij, (left, right, down, up, le, de, angle_e, lon_e, lat_e),
168                       (index(x,y), index(x+1,y), index(x,y-1), index(x,y), dy, dx, 0., x+.5, y ) )
169                put(ij,trisk_deg,(trisk,wee),(
170                    (indexv(x,y),.25),
171                    (indexv(x+1,y),.25),
172                    (indexv(x,y-1),.25),
173                    (indexv(x+1,y-1),.25)))
174                # v-points
175                ij = indexv(x,y)-1
176                putval(ij, (left, right, down, up, le, de, angle_e, lon_e, lat_e),
177                       (index(x,y), index(x,y+1), index(x,y), index(x-1,y), dx, dy, .5*math.pi, x, y+.5 ) )
178                put(ij,trisk_deg,(trisk,wee),(
179                    (indexu(x,y),-.25),
180                    (indexu(x-1,y),-.25),
181                    (indexu(x,y+1),-.25),
182                    (indexu(x-1,y+1),-.25)))
183
184        primal_i, primal_j = [ x.astype(np.int32) for x in lon_i, lat_i]
185        edge_i, edge_j = [ x.astype(np.int32) for x in lon_e, lat_e]
186        lon_i, lon_v, lon_e = [(x+.5)*dx-.5*Lx for x in lon_i, lon_v, lon_e]
187        lat_i, lat_v, lat_e = [(y+.5)*dy-.5*Ly for y in lat_i, lat_v, lat_e]
188
189        Aiv[:]=dx*dy # Ai=Av=dx*dy
190        le_de, fv = le/de, f+0.*Aiv
191       
192        self.set_members(locals(), 'primal_num',  'dual_num', 'edge_num',
193                         'primal_deg', 'primal_edge', 'primal_ne', 'primal_vertex',
194                         'primal_bounds_lon', 'primal_bounds_lat',
195                         'dual_deg', 'dual_edge', 'dual_ne', 'dual_vertex',
196                         'dual_bounds_lon', 'dual_bounds_lat',
197                         'left','right','down','up',
198                         'trisk_deg','trisk','Riv2','wee',
199                         'lon_i','lat_i','lon_v','lat_v', 'fv',
200                         'le_de','le','de','lon_e','lat_e','angle_e',
201                         'primal_i','primal_j','edge_i','edge_j')
202        self.Ai, self.Av = Aiv, Aiv
203        self.to_dynamico()
204    def to_dynamico(self): pass
205    def ncwrite(self, name):
206        """The method writes Cartesian mesh on the disc.
207            Args: Mesh parameters"""
208
209        #----------------OPENING NETCDF OUTPUT FILE------------
210
211        try:
212            f = cdf.Dataset(name, 'w', format='NETCDF4')
213        except:
214            ERROR("Cartesian_mesh.ncwrite : Error occurred while opening new netCDF mesh file")
215            raise
216
217        #----------------DEFINING DIMENSIONS--------------------
218
219        for dimname, dimsize in [("primal_cell", self.primal_deg.size),
220                                 ("dual_cell", self.dual_deg.size),
221                                 ("edge", self.left.size),
222                                 ("primal_edge_or_vertex", self.primal_edge.shape[1]),
223                                 ("dual_edge_or_vertex", self.dual_edge.shape[1]),
224                                 ("TWO", 2),
225                                 ("trisk_edge", 4)]:
226            f.createDimension(dimname,dimsize)
227
228        #----------------DEFINING VARIABLES---------------------
229       
230        f.description = "Cartesian_mesh"
231        f.meshtype, f.nx, f.ny = 'curvilinear', self.nx, self.ny
232
233        def create_vars(dimname, info):
234            for vname, vtype, vdata in info:
235                var = f.createVariable(vname,vtype,dimname)
236                var[:] = vdata
237               
238        create_vars("primal_cell", 
239                    [("primal_deg","i4",self.primal_deg), 
240                     ("primal_i","i4",self.primal_i),
241                     ("primal_j","i4",self.primal_j),
242                     ("Ai","f8",self.Ai),
243                     ("lon_i","f8",self.lon_i),
244                     ("lat_i","f8",self.lat_i)] )
245
246        create_vars("dual_cell", 
247                    [("dual_deg","i4",self.dual_deg), 
248                     ("Av","f8",self.Av),
249                     ("lon_v","f8",self.lon_v),
250                     ("lat_v","f8",self.lat_v),
251                     ] )
252
253        create_vars( ("primal_cell","primal_edge_or_vertex"), 
254                     [("primal_edge", "i4", self.primal_edge),
255                      ("primal_ne", "i4", self.primal_ne),
256                      ("primal_vertex", "i4", self.primal_vertex),
257                      ("primal_bounds_lon", "f8", self.primal_bounds_lon),
258                      ("primal_bounds_lat", "f8", self.primal_bounds_lat)] )
259
260        create_vars( ("dual_cell","dual_edge_or_vertex"), 
261                     [("dual_edge", "i4", self.dual_edge),
262                      ("dual_vertex","i4",self.dual_vertex),
263                      ("dual_bounds_lon", "f8", self.dual_bounds_lon),
264                      ("dual_bounds_lat", "f8", self.dual_bounds_lat),
265                      ("dual_ne","i4",self.dual_ne),
266                      ("Riv2","f8",self.Riv2)] )
267
268        create_vars("edge",
269                    [("trisk_deg","i4",self.trisk_deg),
270                     ("le","f8",self.le),
271                     ("de","f8",self.de),
272                     ("lon_e","f8", self.lon_e),
273                     ("lat_e","f8", self.lat_e),
274                     ("angle_e","f8", self.angle_e),
275                     ("edge_i","i4",self.edge_i),
276                     ("edge_j","i4",self.edge_j)] )
277
278        create_vars( ("edge","TWO"),
279                     [("edge_left_right","i4", concat(self.left,self.right)),
280                      ("edge_down_up","i4", concat(self.down,self.up))] )
281                     
282
283        create_vars( ("edge","trisk_edge"),
284                    [("trisk","i4",self.trisk),
285                     ("wee","f8",self.wee)] )
286
287        f.close()
288
289    def field_theta(self,n=1): return self.zeros((n,self.nqdyn,self.ny,self.nx,self.llm))
290    def field_mass(self,n=1): return self.zeros((n,self.ny,self.nx,self.llm))
291    def field_z(self,n=1): return self.zeros((n,self.ny,self.nx,self.llm))
292    def field_w(self,n=1): return self.zeros((n,self.ny,self.nx,self.llm+1))
293    def field_u(self,n=1): return self.zeros((n,self.ny,2*self.nx,self.llm))
294    def field_ps(self,n=1): return self.zeros((n,self.ny,self.nx))
295    def field_ucomp(self,n=1): return self.zeros((n,self.ny,self.nx,self.llm))
296    def ucomp(self,u): 
297        return u[range(0,2*self.nx,2),:] if self.ny==1 else u[:,range(0,2*self.nx,2),:]
298    def set_ucomp(self,uv,u):
299        if self.ny==1 : uv[range(0,2*self.nx,2),:]=u
300        else : uv[:,range(0,2*self.nx,2),:]=u
301    def vcomp(self,u):
302        return u[range(1,2*self.nx,2),:] if self.ny==1 else u[:,range(1,2*self.nx,2),:]
303    def set_vcomp(self,uv,v):
304        if self.ny==1 : uv[range(1,2*self.nx,2),:]=v
305        else : uv[:,range(1,2*self.nx,2),:]=v
306
307#---------------------- DYNAMICO format for fully unstructured mesh and curvilinear meshes ----------------------
308
309class Mesh_Format(BaseMesh):
310    def getdims(self,*names): return [len(self.nc.dimensions[name]) for name in names]
311    def get_pdims(self,comm,*names): return [self.get_pdim(comm,name) for name in names]
312    def get_pvars(self,pdim,*names): return [self.get_pvar(pdim,name) for name in names]
313    def getvars(self,*names): 
314            for name in names : DEBUG("getvar %s ..."%name)
315            time1=time.time()
316            ret=[self.getvar(name) for name in names]
317            DEBUG("... Done in %f seconds"%(time.time()-time1))
318            return ret
319   
320class DYNAMICO_Format(Mesh_Format):
321    """ Helps class Unstructured_Mesh to read a DYNAMICO mesh file."""
322    start_index=1 # indexing starts at 1 as in Fortran
323    def __init__(self,gridfilename):
324        nc = cdf.Dataset(gridfilename, "r")
325        self.nc, self.meshtype = nc, nc.meshtype
326        if self.meshtype == 'curvilinear':
327            self.nx, self.ny = nc.nx, nc.ny
328    def get_pdim(self,comm,name): return parallel.PDim(self.nc.dimensions[name], comm)
329    def getvar(self,name): return self.nc.variables[name][:]
330    def get_pvar(self,pdim,name):
331        # provides parallel access to a NetCDF variable, with name translation
332        # first deal with special cases
333        if name == 'edge_deg': return parallel.CstPArray1D(pdim, np.int32, 2)
334        # general case
335        return parallel.PArray(pdim, self.nc.variables[name])
336    def normalize(self, mesh): pass
337
338#---------------------- MPAS fully unstructured mesh ------------------------
339
340class MPAS_Format(Mesh_Format):
341    """ Helps class Unstructured_Mesh to read a MPAS mesh file."""
342    start_index=1 # indexing starts at 1 as in Fortran
343    meshtype='unstructured'
344    translate= {
345        'primal_cell':'nCells', 'edge':'nEdges', 'dual_cell':'nVertices',
346        'primal_deg':'nEdgesOnCell', 'trisk_deg':'nEdgesOnEdge',
347        'primal_edge':'edgesOnCell', 'dual_edge':'edgesOnVertex', 
348        'primal_vertex':'verticesOnCell', 'dual_vertex':'cellsOnVertex',
349        'trisk':'edgesOnEdge', 'edge_down_up':'verticesOnEdge', 'edge_left_right':'cellsOnEdge',
350        'le':'dvEdge', 'de':'dcEdge', 'Ai':'areaCell', 'Av':'areaTriangle',
351        'lat_i':'latCell','lon_i':'lonCell','lat_v':'latVertex','lon_v':'lonVertex',
352        'lat_e':'latEdge','lon_e':'lonEdge','angle_e':'angleEdge',
353        'wee':'weightsOnEdge','Riv2':'kiteAreasOnVertex',
354        'primal_ne':'signOnCell', 'dual_ne':'signOnVertex',
355        'primal_bounds_lon':None, 'primal_bounds_lat':None,
356        'dual_bounds_lon':None, 'dual_bounds_lat':None }
357    def __init__(self,gridfilename):
358        try:
359            self.nc = cdf.Dataset(gridfilename, "r")
360        except RuntimeError:
361            ERROR("""
362Unable to open grid file %s, maybe you forgot to download it ?
363To do so, go to the 'Python/' dir and execute './get_MPAS_grids.sh'.
364            """ % gridfile)
365            raise
366    def get_pdim(self,comm,name): 
367        name = self.translate[name]
368        return parallel.PDim(self.nc.dimensions[name], comm)
369    def getvar(self,name):
370        # first deal with special cases
371        if name == 'dual_deg':           
372            dual_deg = [3 for i in range(self.dual_num) ]
373            dual_deg = np.ascontiguousarray(dual_deg,dtype=np.int32) 
374            # NB : Fortran code expects 32-bit ints
375            return dual_deg
376        # general case
377        name=self.translate[name]
378        return self.nc.variables[name][:]
379    def get_pvar(self,pdim,name): 
380        # provides parallel access to a NetCDF variable, with name translation
381        # first deal with special cases
382        if name == 'dual_deg': return parallel.CstPArray1D(pdim, np.int32, 3)
383        if name == 'edge_deg': return parallel.CstPArray1D(pdim, np.int32, 2)
384        # general case
385        name=self.translate[name]
386        return None if name is None else parallel.PArray(pdim, self.nc.variables[name])
387    def normalize(self, mesh):
388        (trisk_deg, trisk, Ai,
389         de, le, wee, Av, Riv2) = (mesh.trisk_deg, mesh.trisk, mesh.Ai, 
390                                       mesh.de, mesh.le, mesh.wee, mesh.Av, mesh.Riv2)
391        edge_num, dual_num = de.size, Av.size
392        # fix normalization of wee and Riv2 weights
393        for edge1 in range(edge_num):
394            for i in range(trisk_deg[edge1]):
395                edge2=trisk[edge1,i]-1  # NB Fortran vs C/Python indexing
396                wee[edge1,i] = de[edge1]*wee[edge1,i]/le[edge2]
397        for ivertex in range(dual_num):
398            for j in range(3):
399                Riv2[ivertex,j]=Riv2[ivertex,j]/Av[ivertex]
400            r=Riv2[ivertex,:]
401            r=sum(r)
402            if abs(r-1.)>1e-6 : WARNING('error with Riv2 at vertex %d'%ivertex)
403        # scale lengths and areas
404        # this is now done by apply_map
405        # mesh.le, mesh.de, mesh.Av, mesh.Ai = radius*le, radius*de, r2*Av, r2*Ai
406    def get_bounds(self, mesh):
407        def bounds(deg, vertex, lon, lat):
408            num, maxdeg = deg.size, deg.max()
409            vertex_lon, vertex_lat = np.zeros((num, maxdeg)), np.zeros((num, maxdeg))
410            for cell in range(num):
411                cdeg=deg[cell]
412                for ivertex in range(maxdeg):
413                    if ivertex<cdeg:
414                        vtx = vertex[cell,ivertex] - 1
415                        vlon, vlat = lon[vtx], lat[vtx]
416                    vertex_lon[cell, ivertex] = vlon
417                    vertex_lat[cell, ivertex] = vlat
418            return vertex_lon, vertex_lat
419        mesh.primal_bounds_lon, mesh.primal_bounds_lat = bounds(mesh.primal_own_deg, mesh.primal_vertex, mesh.lon_v, mesh.lat_v)
420        mesh.dual_bounds_lon, mesh.dual_bounds_lat = bounds(mesh.dual_deg, mesh.dual_vertex, mesh.lon_i, mesh.lat_i)
421
422def compute_ne(num,deg,edges,left,right):
423    ne = np.zeros_like(edges)
424    for cell in range(num):
425        for iedge in range(deg[cell]):
426            edge = edges[cell,iedge]-1
427            if left[edge]==cell+1: ne[cell,iedge]+=1
428            if right[edge]==cell+1: ne[cell,iedge]-=1
429#            if ne[cell,iedge]==0 : print 'compute_ne error at cell,iedge', cell, iedge
430    return ne
431
432class Abstract_Mesh(BaseMesh):
433    def to_dynamico(self): pass
434    def field_theta(self,n=1): return self.zeros((n,self.nqdyn,self.primal_num,self.llm))
435    def field_mass(self,n=1):  return self.zeros((n,self.primal_num,self.llm))
436    def field_z(self,n=1):     return self.zeros((n,self.dual_num,self.llm))
437    def field_w(self,n=1):     return self.zeros((n,self.primal_num,self.llm+1))
438    def field_u(self,n=1):     return self.zeros((n,self.edge_num,self.llm))
439    def field_ps(self,n=1):    return self.zeros((n,self.primal_num,))
440    def ucov2D(self, ulon, ulat): 
441        return self.de*(ulon*np.cos(self.angle_e)+ulat*np.sin(self.angle_e))
442    def ucov3D(self, ulon, ulat):
443        ucov = self.zeros((self.edge_num,ulon.shape[1]))
444        for edge in range(self.edge_num):
445            angle=self.angle_e[edge]
446            ucov[edge,:] = self.de[edge]*(ulon[edge,:]*math.cos(angle)+ulat[edge,:]*math.sin(angle))
447        return ucov
448    def plot(self, tri,data, **kwargs):
449        plt.figure(figsize=(10,4))
450        plt.gca().set_aspect('equal')
451        plt.tricontourf(tri, data, 20, **kwargs)
452        plt.colorbar()
453        plt.ylim((-90,90))
454        plt.xlim((0,360))
455    def plot_i(self,data, **kwargs):
456        self.plot(self.primal,data, **kwargs)
457    def plot_v(self,data, **kwargs):
458        self.plot(self.dual,data, **kwargs)
459    def plot_e(self,data, **kwargs):
460        self.plot(self.triedge,data, **kwargs)
461    def apply_map(self, mapping):
462        """Before calling apply_map, lat and lon are coordinates in the reference domain.
463        After calling apply_map, lat and lon are coordinates in the physical domain."""
464        lat_i, lat_e, lat_v = self.lat_i, self.lat_e, self.lat_v
465        lon_i, lon_e, lon_v = self.lon_i, self.lon_e, self.lon_v
466        factor_e = mapping.map_factor(lon_e, lat_e)
467        factor2_i = mapping.map_factor(lon_i, lat_i)**2
468        factor2_v = mapping.map_factor(lon_v, lat_v)**2
469        self.le, self.de, self.Av, self.Ai = self.le*factor_e, self.de*factor_e, self.Av*factor2_v, self.Ai*factor2_i
470        self.angle_e += mapping.map_angle(lon_e, lat_e)
471        self.lon_i, self.lat_i = mapping.map(lon_i, lat_i)
472        self.lon_v, self.lat_v = mapping.map(lon_v, lat_v)
473        self.lon_e, self.lat_e = mapping.map(lon_e, lat_e)
474        self.primal_bounds_lon, self.primal_bounds_lat = mapping.map( self.primal_bounds_lon, self.primal_bounds_lat)
475        self.dual_bounds_lon, self.dual_bounds_lat = mapping.map( self.dual_bounds_lon, self.dual_bounds_lat)
476        self.ref_lon_i, self.ref_lat_i = lon_i, lat_i
477        self.ref_lon_e, self.ref_lat_e = lon_e, lat_e
478        self.ref_lon_v, self.ref_lat_v = lon_v, lat_v
479
480class Unstructured_Mesh(Abstract_Mesh):
481    def __init__(self, gridfile, llm, nqdyn, radius, f):
482        getvars = gridfile.getvars
483        # get positions, lengths, surfaces and weights
484        le,de,Ai,Av,wee,Riv2 = getvars('le','de','Ai','Av','wee','Riv2')
485        le_de = le/de
486        lat_i,lon_i,lat_v,lon_v = getvars('lat_i','lon_i','lat_v','lon_v')
487        lat_e,lon_e,angle_e = getvars('lat_e','lon_e','angle_e')
488        fv = f(lon_v,lat_v) # Coriolis parameter
489        primal_num, dual_num, edge_num = [ x.size for x in Ai, Av, le ]
490        gridfile.primal_num, gridfile.dual_num = primal_num, dual_num
491        primal_deg, dual_deg, trisk_deg = getvars('primal_deg','dual_deg','trisk_deg')       
492        # get indices for stencils
493        # primal <-> edges
494        primal_edge, left_right = getvars('primal_edge','edge_left_right')
495        left,right = left_right[:,0], left_right[:,1]
496        primal_ne = compute_ne(primal_num,primal_deg,primal_edge,left,right)
497        # dual <-> edges
498        dual_edge, down_up = getvars('dual_edge','edge_down_up')
499        down,up = down_up[:,0], down_up[:,1]
500        dual_ne = compute_ne(dual_num,dual_deg,dual_edge,up,down)
501        # primal <-> dual, edges <-> edges (TRiSK)
502        primal_vertex, dual_vertex, trisk = getvars('primal_vertex','dual_vertex','trisk')
503        # CRITICAL : force arrays left, etc. to be contiguous in memory:
504        left,right,up,down = [np.ascontiguousarray(x) for x in (left,right,up,down)]
505        trisk,wee,primal_edge = [np.ascontiguousarray(x) for x in (trisk,wee,primal_edge)]
506        self.set_members(locals(), 'gridfile','llm','nqdyn','radius','f',
507                         'primal_num', 'dual_num', 'edge_num', 'primal_deg', 'primal_vertex', 'primal_ne',
508                         'lat_i', 'lon_i', 'lat_v', 'lon_v', 'lat_e', 'lon_e', 'angle_e',
509                         'Ai', 'Av', 'fv', 'le', 'de', 'down', 'up', 'le_de',
510                         'trisk_deg', 'trisk', 'wee', 'Riv2', 'dual_ne',
511                         'left', 'right',
512                         'primal_edge')
513        gridfile.normalize(self)
514        self.to_dynamico()
515        self.primal  = Triangulation(lon_i, lat_i)
516        self.dual    = Triangulation(lon_v, lat_v)
517        self.triedge = Triangulation(lon_e, lat_e)
518       
519class Unstructured_PMesh(Abstract_Mesh): # Mesh data distributed across MPI processes
520    def __init__(self,comm, gridfile):
521        self.comm, self.gridfile, get_pvars = comm, gridfile, gridfile.get_pvars
522        dim_primal, dim_edge, dim_dual = gridfile.get_pdims(comm,
523            'primal_cell','edge','dual_cell')
524        primal_deg, primal_vertex, primal_edge, primal_ne, lon_i, lat_i, Ai = get_pvars(
525            dim_primal, 'primal_deg', 'primal_vertex', 'primal_edge', 'primal_ne', 'lon_i', 'lat_i', 'Ai' )
526        primal_bounds_lon, primal_bounds_lat = get_pvars(dim_primal, 'primal_bounds_lon', 'primal_bounds_lat')
527        edge_deg, trisk_deg, left_right, down_up, trisk, le, de, wee, lon_e, lat_e, angle_e = get_pvars(
528             dim_edge, 'edge_deg', 'trisk_deg', 'edge_left_right', 'edge_down_up', 
529             'trisk', 'le', 'de', 'wee', 'lon_e', 'lat_e', 'angle_e')
530        dual_deg, dual_vertex, dual_edge, dual_ne, Riv2, lon_v, lat_v, Av = get_pvars(
531            dim_dual, 'dual_deg', 'dual_vertex', 'dual_edge', 'dual_ne', 'Riv2', 'lon_v', 'lat_v', 'Av')
532        dual_bounds_lon, dual_bounds_lat = get_pvars(dim_dual, 'dual_bounds_lon', 'dual_bounds_lat')
533        if gridfile.meshtype == 'curvilinear':
534            self.primal_i, self.primal_j = get_pvars(dim_primal, 'primal_i','primal_j')
535            self.dual_i, self.dual_j = get_pvars(dim_dual, 'primal_i','primal_j')
536            self.edge_i, self.edge_j = get_pvars(dim_edge, 'edge_i','edge_j')
537
538        # Let indices start at 0
539        for x in (primal_vertex, primal_edge, dual_vertex,dual_edge,
540            left_right, down_up, trisk ) : x.data = x.data-gridfile.start_index
541        self.edge2cell   = Stencil_glob(edge_deg, left_right)
542        self.cell2edge   = Stencil_glob(primal_deg, primal_edge, primal_ne)
543        self.cell2vertex = Stencil_glob(primal_deg, primal_vertex)
544        self.edge2vertex = Stencil_glob(edge_deg, down_up)
545        self.vertex2edge = Stencil_glob(dual_deg, dual_edge, dual_ne)
546        self.vertex2cell = Stencil_glob(dual_deg, dual_vertex, Riv2)
547        self.edge2edge   = Stencil_glob(trisk_deg, trisk, wee)
548        self.set_members(locals(), 'dim_primal', 'dim_dual', 'dim_edge',
549                         'primal_deg', 'primal_vertex', 'primal_edge',
550                         'primal_bounds_lon', 'primal_bounds_lat',
551                         'trisk_deg', 'trisk', 'dual_deg', 'dual_edge',
552                         'dual_bounds_lon', 'dual_bounds_lat',
553                         'lon_v', 'lat_v', 'Av', 'lon_i', 'lat_i', 'Ai', 
554                         'le', 'de', 'lon_e', 'lat_e', 'angle_e')
555    def partition_metis(self):
556        INFO('Partitioning with ParMetis...')
557# TODO        edge_owner = partition_mesh(self.comm, self.trisk_deg, self.trisk)
558        edge_owner = partition_mesh(self.trisk_deg, self.trisk, self.comm.Get_size())
559        self.edge_owner = parallel.LocPArray1D(self.dim_edge, edge_owner)
560        primal_owner = partition_from_stencil(self.edge_owner, self.primal_deg, self.primal_edge)
561        self.primal_owner = parallel.LocPArray1D(self.dim_primal, primal_owner)
562        dual_owner = partition_from_stencil(self.edge_owner, self.dual_deg, self.dual_edge)
563        self.dual_owner = parallel.LocPArray1D(self.dim_dual, dual_owner)
564
565    def partition_curvilinear(self, ni,nj):
566        def owner(dim,i,j):
567            owner_i, owner_j =  (ni*i.data)/nx, (nj*j.data)/ny
568            return parallel.LocPArray1D(dim, owner_i + ni*owner_j)
569        nx, ny = self.gridfile.nx, self.gridfile.ny
570        INFO('Partitioning %d x %d cells in %d x %d blocks ...'%(nx,ny,ni,nj))
571        n = self.comm.Get_size()
572        assert n == ni*nj, 'Mismatch between ni x nj = %d and MPI_SIZE = %d.'%(ni*nj, n)
573        self.edge_owner   = owner(self.dim_edge,   self.edge_i,   self.edge_j)
574        self.primal_owner = owner(self.dim_primal, self.primal_i, self.primal_j)
575        self.dual_owner = self.primal_owner
576        DEBUG('partition_curvilinear %d : %s'%(self.comm.Get_rank(), self.primal_owner.data) )
577
578    def get(self, getter, *names): return [ getter(self.__dict__[x]) for x in names ]
579
580def chain(start, links):
581    for link in links:
582        start = link(start).neigh_set
583        yield start
584
585def patches(degree, bounds, lon, lat):
586    for i in range(degree.size):
587        nb_edge=degree[i]
588        bounds_cell = bounds[i,0:nb_edge]
589        lat_cell    = lat[bounds_cell]
590        lon_cell    = lon[bounds_cell]
591        orig=lon_cell[0]
592        lon_cell    = lon_cell-orig+180.
593        lon_cell    = np.mod(lon_cell,360.)
594        lon_cell    = lon_cell+orig-180.
595#        if np.abs(lon_cell-orig).max()>10. :
596#            print '%d patches :'%mpi_rank, lon_cell
597        lonlat_cell = np.zeros((nb_edge,2))
598        lonlat_cell[:,0],lonlat_cell[:,1] = lon_cell,lat_cell
599        polygon = Polygon(lonlat_cell, True)
600        yield polygon
601
602class Local_Mesh(Abstract_Mesh):
603    Halo_Xchange = parallel.Halo_Xchange # overriden in dev.meshes
604    def __init__(self, pmesh, llm, nqdyn, mapping):
605        comm, dim_primal, primal_owner, dim_edge, edge_owner, dim_dual, dual_owner = pmesh.members(
606            'comm', 'dim_primal', 'primal_owner', 'dim_edge', 'edge_owner', 'dim_dual', 'dual_owner' )
607        INFO('Constructing halos ...')
608        edges_E0 = find_my_cells(edge_owner)
609        cells_C0, edges_E1, vertices_V1, edges_E2, cells_C1 = chain(
610            edges_E0, ( pmesh.edge2cell, pmesh.cell2edge, pmesh.edge2vertex, pmesh.vertex2edge, pmesh.edge2cell) )
611        edges_E0, edges_E1, edges_E2 = progressive_list(edges_E0, edges_E1, edges_E2)
612        cells_C0, cells_C1 = progressive_list(cells_C0, cells_C1)
613        DEBUG('E2,E1,E0 ; C1,C0 : %s' % map(len, (edges_E2, edges_E1, edges_E0, cells_C1, cells_C0)) )
614        get_own_cells, get_all_cells = dim_primal.getter(cells_C0), dim_primal.getter(cells_C1)
615        get_all_duals, get_all_edges = dim_dual.getter(vertices_V1), dim_edge.getter(edges_E2)
616
617        primal_num, dual_num, edge_num = map(len, (cells_C1, vertices_V1, edges_E2))
618
619        self.meshtype = pmesh.gridfile.meshtype
620        if self.meshtype == 'curvilinear' :
621            self.nx, self.ny = pmesh.gridfile.nx, pmesh.gridfile.ny
622            self.primal_i, self.primal_j = pmesh.get(get_all_cells, 'primal_i', 'primal_j')
623            self.dual_i, self.dual_j = pmesh.get(get_all_duals, 'dual_i', 'dual_j')
624
625        # construct local stencils from global stencils
626        dict_E2, dict_C1, dict_V1 = map(inverse_list, (edges_E2, cells_C1, vertices_V1))
627        down_up       = pmesh.edge2vertex( edges_E2,    dict_V1)
628        left_right    = pmesh.edge2cell(   edges_E2,    dict_C1)
629        primal_edge   = pmesh.cell2edge(   cells_C1,    dict_E2)
630        dual_edge     = pmesh.vertex2edge( vertices_V1, dict_E2)
631        trisk         = pmesh.edge2edge(   edges_E2,    dict_E2)
632        Riv2          = pmesh.vertex2cell( vertices_V1, dict_C1)
633        DEBUG('Edge-vertex degree min/max %d %d'%(down_up.degree.min(), down_up.degree.max()) )
634        DEBUG('Edge-cell degree min/max %d %d'%(left_right.degree.min(), left_right.degree.max()) )
635        DEBUG('Primal degree min/max : %d %d'%(primal_edge.degree.min(), primal_edge.degree.max()) )
636        DEBUG('Dual degree min/max : %d %d'%(dual_edge.degree.min(), dual_edge.degree.max()) )
637        DEBUG('TRiSK degree min/max : %d %d'%(trisk.degree.min(), trisk.degree.max()) )
638        DEBUG('Riv2 degree min/max : %d %d'%(Riv2.degree.min(), Riv2.degree.max()) )
639        primal_deg, primal_edge, primal_ne = primal_edge.degree,   primal_edge.neigh_loc, primal_edge.weight
640        dual_deg,   dual_edge, dual_ne  = dual_edge.degree, dual_edge.neigh_loc, dual_edge.weight
641        trisk_deg,  trisk, wee      = trisk.degree,         trisk.neigh_loc, trisk.weight
642        dual_deg, dual_vertex, Riv2 = Riv2.degree,          Riv2.neigh_loc,  Riv2.weight
643        edge_deg, left, right = left_right.degree, left_right.neigh_loc[:,0], left_right.neigh_loc[:,1]
644        edge_deg, down, up    = down_up.degree,    down_up.neigh_loc[:,0],    down_up.neigh_loc[:,1]
645        for edge in range(edge_num): 
646            if edge_deg[edge]<2: up[edge]=down[edge]
647
648        # construct own primal mesh for XIOS output
649        primal_own_glo = find_my_cells(primal_owner)
650        primal_vertex = pmesh.cell2vertex(primal_own_glo, dict_V1)
651        primal_own_deg, primal_vertex = primal_vertex.degree, primal_vertex.neigh_loc
652        primal_own_glo = np.asarray(primal_own_glo, dtype=np.int32)
653        primal_own_loc = [dict_C1[i] for i in primal_own_glo]
654        primal_own_num = len(primal_own_glo)
655        primal_own_all = comm.allgather(primal_own_num)
656        displ = sum(primal_own_all[0:comm.Get_rank()]) # required by XIOS
657#        print 'local primal mesh :', primal_vertex.min(), primal_vertex.max(), primal_own_all, displ
658
659        # construct own dual mesh for XIOS output
660        dual_own_glo = find_my_cells(dual_owner)
661        dual_own_glo = np.asarray(dual_own_glo, dtype=np.int32)
662        dual_own_loc = [dict_V1[i] for i in dual_own_glo]
663       
664        # compute_ne(...) and normalize(...) expect indices starting at 1
665        for ind in (trisk, primal_vertex, dual_vertex, primal_edge, dual_edge) : ind+=1
666        left, right, down, up = left+1, right+1, down+1, up+1 # these ones are subarrays, we need deep copies
667
668        # read from mesh file : coordinates, lengths and areas in reference domain
669        lon_i, lat_i, Ai = pmesh.get(get_all_cells, 'lon_i', 'lat_i', 'Ai')
670
671
672        lon_v, lat_v, Av = pmesh.get(get_all_duals, 'lon_v', 'lat_v', 'Av')
673        le, de, lon_e, lat_e, angle_e = pmesh.get(get_all_edges, 'le', 'de', 'lon_e', 'lat_e', 'angle_e')
674        le_de = le/de
675
676        # store what we have read/computed
677        self.set_members(locals(), 'nqdyn', 'llm', 'primal_num', 'dual_num', 'edge_num',
678                         'primal_own_glo', 'primal_own_loc', 'primal_own_deg',
679                         'primal_deg', 'primal_vertex', 'displ', 'dual_own_loc', 
680                         'trisk_deg', 'trisk', 'wee',
681                         'dual_deg', 'dual_edge', 'dual_ne', 'dual_vertex', 'Riv2',
682                         'left', 'right', 'down', 'up', 
683                         'primal_deg', 'primal_edge', 'primal_ne',
684                         'vertices_V1', 'edges_E2',
685                         'lon_i', 'lat_i', 'Ai',
686                         'lon_v', 'lat_v', 'Av',
687                         'le', 'de', 'le_de', 'lon_e', 'lat_e', 'angle_e')
688
689        # obtain bounds of primal and dual cells (for XIOS)
690        if pmesh.primal_bounds_lon is not None:
691            INFO('Reading cell bounds from mesh file.')
692            self.primal_bounds_lon, self.primal_bounds_lat = pmesh.get(get_all_cells, 'primal_bounds_lon', 'primal_bounds_lat')
693            self.dual_bounds_lon, self.dual_bounds_lat = pmesh.get(get_all_duals, 'dual_bounds_lon', 'dual_bounds_lat')
694        else:
695            INFO('Mesh file does not contain information abound cell bounds.')
696            pmesh.gridfile.get_bounds(self)
697
698        # normalize data read from file depending on file format
699        pmesh.gridfile.normalize(self)
700        # map from reference domain to physical domain
701        self.apply_map(mapping)
702        # now latitudes and longitudes refer to the physical domain
703
704        self.fv = mapping.coriolis(self.lon_v,self.lat_v) # Coriolis parameter
705        # propagate info to Fortran side
706        self.to_dynamico()
707        # setup halo transfers - NB : llm must be set before we call set_dynamico_transfer
708        self.com_primal = self.Halo_Xchange(
709            42, dim_primal, cells_C1, get_all_cells(primal_owner))
710        self.com_edges = self.Halo_Xchange(
711            73, dim_edge, edges_E2, get_all_edges(edge_owner))
712        self.com_primal.set_dynamico_transfer('primal')
713        self.com_edges.set_dynamico_transfer('edge')
714
715        self.primal  = Triangulation(lon_i, self.lat_i)
716        self.dual    = Triangulation(lon_v, self.lat_v)
717        self.triedge = Triangulation(lon_e, self.lat_e)       
718
719    def plot_patches(self, ax, clim, degree, bounds, lon, lat, data):
720        nb_vertex = lon.size # global
721        p = list(patches(degree, bounds, lon, lat))
722        p = PatchCollection(p, linewidth=0.01)
723        p.set_array(data) # set values at each polygon (cell)
724        p.set_clim(clim)
725        ax.add_collection(p)
726
727#--------------------------------------  Mesh reordering  ------------------------------------------#
728
729# NB : Indices start at 0
730# permutations map an old index to a new index : perm[old_index]=new_index
731# enumerate(perm) is the list [ (0,perm[0]) , (1,perm[1]), ...] = [(old, new) ... ]
732
733def reorder_values(perm, *datas): 
734    # data in datas : ndarray containing indices ; perm : permutation to apply to values
735    for data in datas:
736        for x in np.nditer(data, op_flags=['readwrite']):
737            x[...]=perm[x]
738
739def reorder_indices(perm, *datas): 
740    # data in datas : ndarray containing values ; perm : permutation to apply to indices
741    for data in datas:
742        data_out, ndim = data.copy(), data.ndim
743        if ndim == 1:
744            for old,new in enumerate(perm):
745                data_out[new]=data[old]
746        if ndim == 2:
747            for old,new in enumerate(perm):
748                data_out[new,:]=data[old,:]
749        data[:]=data_out[:]
750
751def key_to_perm(keys): 
752    # keys : maps an old index to a key
753    # returns : a list mapping old indices to new indices obtained by sorting keys in ascending order
754    ordered = [old for key,old in sorted(zip(keys,range(len(keys))))] 
755    # ordered maps new indices to old indices
756    # but we want to map old indices to new indices
757    perm = list(range(len(keys)))
758    for new,old in enumerate(ordered):
759        perm[old]=new
760    return perm  # maps old indices to new indices
761
762#-------------------------------------- Mesh partitioning ------------------------------------------#
763
764# NB : Indices start at 0
765
766def partition_from_stencil(owner2, degree, stencil):
767    # given a stencil dim1->dim2 and owner2 on dim2, define owner[i] on dim1 as min(stencil[i,:] if i is even, max if odd
768    dim1, dim2= degree.dim, owner2.dim
769    degree, stencil, n = degree.data, stencil.data, dim1.end-dim1.start
770    cells2 = list_stencil(degree, stencil)
771    cells2 = sorted(list(set(list(cells2)))) # list of cells for which we need to know owner2
772    get2 = dim2.getter(cells2)
773    owner1, owner2, glob2loc = np.zeros(n, dtype=np.int32), get2(owner2), get2.dict
774    for i in range(n):
775        owners = [ owner2[glob2loc[stencil[i,j]]] for j in range(degree[i]) ]
776        if i%2 == 0 : owner1[i] = min(owners)
777        else : owner1[i] = max(owners)
778    return owner1
779           
780def find_my_cells(owner): # owner : a PArray1D containing the data returned by partition_mesh
781    dim, comm, owner = owner.dim, owner.dim.comm, owner.data
782    mpi_rank, mpi_size = comm.Get_rank(), comm.Get_size()
783    cells=[set() for i in range(mpi_size)]
784    for i in range(len(owner)):
785        cells[owner[i]].add(dim.start+i)
786    cells = [sorted(list(cells[i])) for i in range(mpi_size)]
787    mycells = comm.alltoall(cells)
788    mycells = sorted(sum(mycells, [])) # concatenate into a single list
789    return mycells
790
791#---------------------------------- Stencil management ---------------------------------------#
792
793# NB : Indices start at 0
794
795# Class Stencil represents an adjacency relationship (e.g. cell=>edges)
796# using adjacency information read from PArrays
797# It computes a list of "edges" adjacent to a given list of "cells"
798# This is used to form the sets E0 -> C0 -> E1 -> V1 -> E2 -> C1
799# which are then used to form lists of global indices for V1,C1,E2 such that
800# C0, E0, E1 form contiguous subsets of C1, E2 starting from 0
801
802def reindex(vertex_dict, degree, bounds):
803    for i in range(degree.size):
804        for j in range(degree[i]):
805            bounds[i,j] = vertex_dict[bounds[i,j]]
806    return bounds
807
808class Stencil_glob(object):
809    def __init__(self, degree, neigh, weight=None):
810        self.degree, self.neigh, self.weight = degree, neigh, weight
811    def __call__(self, cells, neigh_dict=None):
812        return Stencil(cells, self.degree, self.neigh, neigh_dict, self.weight)
813           
814class Stencil(object):
815    def __init__(self, cells, degree, neigh, neigh_dict, weight=None):
816        get = degree.dim.getter(cells)
817        mydegree, myneigh = [get(x) for x in (degree, neigh)]
818        if not weight is None : myweight = get(weight)
819        if neigh_dict is None : 
820            keep = lambda n : True
821        else : # if neigh_dict is present, only neighbours in dict are retained
822            keep = lambda n : n in neigh_dict
823        neigh_set = list_stencil(mydegree, myneigh, keep)
824        self.neigh_set = list(set(list(neigh_set)     ))                         
825        rej=0
826        for i in range(len(mydegree)): # keep only elements in neigh_dict, in-place
827            k=0
828            for j in range(mydegree[i]):
829                n=myneigh[i,j]
830                if keep(n):
831                    myneigh[i,k]=n
832                    if not weight is None : myweight[i,k]=myweight[i,j]
833                    k=k+1
834                else:
835                    rej=rej+1
836            mydegree[i]=k
837        if neigh_dict is None:
838            neigh_dict = {j:i for i,j in enumerate(self.neigh_set)}
839        myneigh_loc = reindex(neigh_dict, mydegree, myneigh)
840        self.degree, self.neigh_glob, self.neigh_loc = mydegree, myneigh, myneigh_loc
841        if weight is not None: self.weight = myweight
842           
843def progressive_iter(mylist, cell_lists):
844    for thelist in cell_lists: 
845        mylist = mylist + list(set(thelist)-set(mylist))
846        yield mylist
847def progressive_list(*cell_lists):
848    # cell_lists : a tuple of lists of global indices, with each list a subset of the next
849    # returns : a list 'mylist' such that for each list 'thelist' in cell_lists, thelist = mylist[0:len(thelist)]
850    # example : edge_list = progressive_list(E0,E1,E2) with E0,E1,E2 increasing lists of cell edges
851    return list(progressive_iter([], cell_lists))
Note: See TracBrowser for help on using the repository browser.