stomel: spatial tools for ocean modeling

Orthogonal triangular grid generation

Requirements:

  • create a conda environment with python 2.7, numpy 1.8, ipython and ipython-notebook
  • Add rustychris and osgeo to your .condarc channels
  • conda install stomel

Conda is great, but it's still fairly new. The packages have only been generated and tested on a few platforms.

Grid generation with stomel can be used either from a command line program, tom.py, or the programmatic approaches shown here.


In [1]:
%matplotlib inline

In [2]:
# Import a bunch of libraries.
import numpy as np
import matplotlib.pyplot as plt
from stomel import paver
from stomel import field
from stomel import constrained_delaunay

Box and triangle

First example is a 1000 unit box, with a right-triangle island in the middle. The cells will have an average edge-length of 50.


In [3]:
# Define a polygon
boundary=np.array([[0,0],[1000,0],[1000,1000],[0,1000]])
island  =np.array([[200,200],[600,200],[200,600]])

rings=[boundary,island]

In [4]:
# And the scale:
scale=field.ConstantField(50)

In [5]:
p=paver.Paving(rings=rings,density=scale)
p.pave_all() # writes cryptic progress messages - takes about 30s


populate_dt: top
populate_dt: adding points
populate_dt: add constraints
populate_dt: end
populate_dt: top
populate_dt: adding points
populate_dt: 0/7
populate_dt: add constraints
populate_dt: 0/7
populate_dt: end
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 21 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 21 before alpha was reached
Discarding local node because boundary distance less than 25% longer than straight line
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 20 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 20 before alpha was reached
d_boundary 42 => 41 = 103.115 vs line 80.8248
CAREFUL: allowing a nonlocal connection to a nearby node, but okay(?) b/c of distances
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 11 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 11 before alpha was reached
Discarding local node because boundary distance less than 25% longer than straight line
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 10 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 10 before alpha was reached
d_boundary 30 => 29 = 103.09 vs line 80.7976
CAREFUL: allowing a nonlocal connection to a nearby node, but okay(?) b/c of distances
Discarding local node because boundary distance less than 25% longer than straight line
Discarding local node because boundary distance less than 25% longer than straight line
bisect_nonlocal is about to resample preemptively
Crazy - we're on a very small loop. Forcing 3 edges
We're on a very small loop, with one real edge.  Forcing remained to be 2 edges
bisect_nonlocal is about to add an edge
bisect_nonlocal got cells [] from new edge
Iters are from different rings - will join rings together
Discarding local node because boundary distance less than 25% longer than straight line
Discarding local node because boundary distance less than 25% longer than straight line
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 5 before alpha was reached
Discarding local node because boundary distance less than 25% longer than straight line
!No more rings need paving!
Renumbering in preparation for statistics
------ Statistics ------
[None]
CPU user time (unix only):  20.95
CPU sys time  (unix only):  0.0
Wall time                :  20.9400000013
  cells:  849
  edges:  1327
 points:  478
 cells/cpu-second:  40.5250596659
Relative scale error:      -0.189 ---   0.006+- 0.058  ---  0.209
Relative clearance error:  -0.780 ---  -0.024+- 0.268  ---  0.893
 ----- ---------- ----- 
Done!

In [6]:
p.plot().set_color('g')
plt.axis('equal')
plt.title('Constant scale grid')


Out[6]:
<matplotlib.text.Text at 0x7f493bd61fd0>

Telescoping scale

Scales are set by various subclasses of Field, which are simply different ways of representing a scalar function over the $x$-$y$ plane. The simplest is ConstantField, but the most common for grid generation (and the two which are supported by the command-line tom.py script) are ConstrainedDelaunayField and ApolloniusField.

ApolloniusField (named for the datastructure, Apollonius graph) is an automatically telescoping field. You provide points and scales at those points. The scale is allowed to grow (telescope) at a constant rate (defaults to 10%). This is handy when there is some feature which should be resolved at a higher resolution, but you want to gradually return to the background resolution. In the example below, a sinusoidal river is given a higher resolution, and resolution in the rest of the domain is based on the distance to the "river."

The Apollonius graph implementation in CGAL does not get a lot of attention, and is missing from stock python bindings. The rustychris channel python-cgal-bindings has it, though. These are compiled from a fork of python-cgal-bindings.


In [7]:
# Second example - telescoping scale

square=np.array([[0,0],[1000,0],[1000,1000],[0,1000]])
x=np.linspace(0,1000,100)
# make a meandering channel where resolution will be higher
# and let the resolution grow at the default rate (10%)
channel=np.array( [x,300+100*np.cos(x/200.)]).T
channel_scale=field.ApolloniusField(X=channel,F=20*np.ones(len(channel)))


Constructing Apollonius Graph.  quantize=False
        0 /      100
AG::insert: -500.000000,118.340833,-200.000000
            -500.000000,118.340833,-200.000000
AG::insert: -489.898990,118.213323,-200.000000
            -489.898990,118.213323,-200.000000
AG::insert: -479.797980,117.831115,-200.000000
            -479.797980,117.831115,-200.000000
AG::insert: -469.696970,117.195186,-200.000000
            -469.696970,117.195186,-200.000000
AG::insert: -459.595960,116.307156,-200.000000
            -459.595960,116.307156,-200.000000
AG::insert: -449.494949,115.169291,-200.000000
            -449.494949,115.169291,-200.000000
AG::insert: -439.393939,113.784492,-200.000000
            -439.393939,113.784492,-200.000000
AG::insert: -429.292929,112.156291,-200.000000
            -429.292929,112.156291,-200.000000
AG::insert: -419.191919,110.288841,-200.000000
            -419.191919,110.288841,-200.000000
AG::insert: -409.090909,108.186903,-200.000000
            -409.090909,108.186903,-200.000000
AG::insert: -398.989899,105.855837,-200.000000
            -398.989899,105.855837,-200.000000
AG::insert: -388.888889,103.301590,-200.000000
            -388.888889,103.301590,-200.000000
AG::insert: -378.787879,100.530674,-200.000000
            -378.787879,100.530674,-200.000000
AG::insert: -368.686869,97.550156,-200.000000
            -368.686869,97.550156,-200.000000
AG::insert: -358.585859,94.367637,-200.000000
            -358.585859,94.367637,-200.000000
AG::insert: -348.484848,90.991233,-200.000000
            -348.484848,90.991233,-200.000000
AG::insert: -338.383838,87.429554,-200.000000
            -338.383838,87.429554,-200.000000
AG::insert: -328.282828,83.691685,-200.000000
            -328.282828,83.691685,-200.000000
AG::insert: -318.181818,79.787156,-200.000000
            -318.181818,79.787156,-200.000000
AG::insert: -308.080808,75.725926,-200.000000
            -308.080808,75.725926,-200.000000
AG::insert: -297.979798,71.518352,-200.000000
            -297.979798,71.518352,-200.000000
AG::insert: -287.878788,67.175163,-200.000000
            -287.878788,67.175163,-200.000000
AG::insert: -277.777778,62.707436,-200.000000
            -277.777778,62.707436,-200.000000
AG::insert: -267.676768,58.126564,-200.000000
            -267.676768,58.126564,-200.000000
AG::insert: -257.575758,53.444230,-200.000000
            -257.575758,53.444230,-200.000000
AG::insert: -247.474747,48.672375,-200.000000
            -247.474747,48.672375,-200.000000
AG::insert: -237.373737,43.823168,-200.000000
            -237.373737,43.823168,-200.000000
AG::insert: -227.272727,38.908975,-200.000000
            -227.272727,38.908975,-200.000000
AG::insert: -217.171717,33.942329,-200.000000
            -217.171717,33.942329,-200.000000
AG::insert: -207.070707,28.935896,-200.000000
            -207.070707,28.935896,-200.000000
AG::insert: -196.969697,23.902444,-200.000000
            -196.969697,23.902444,-200.000000
AG::insert: -186.868687,18.854807,-200.000000
            -186.868687,18.854807,-200.000000
AG::insert: -176.767677,13.805860,-200.000000
            -176.767677,13.805860,-200.000000
AG::insert: -166.666667,8.768479,-200.000000
            -166.666667,8.768479,-200.000000
AG::insert: -156.565657,3.755509,-200.000000
            -156.565657,3.755509,-200.000000
AG::insert: -146.464646,-1.220266,-200.000000
            -146.464646,-1.220266,-200.000000
AG::insert: -136.363636,-6.146155,-200.000000
            -136.363636,-6.146155,-200.000000
AG::insert: -126.262626,-11.009597,-200.000000
            -126.262626,-11.009597,-200.000000
AG::insert: -116.161616,-15.798190,-200.000000
            -116.161616,-15.798190,-200.000000
AG::insert: -106.060606,-20.499720,-200.000000
            -106.060606,-20.499720,-200.000000
AG::insert: -95.959596,-25.102198,-200.000000
            -95.959596,-25.102198,-200.000000
AG::insert: -85.858586,-29.593887,-200.000000
            -85.858586,-29.593887,-200.000000
AG::insert: -75.757576,-33.963332,-200.000000
            -75.757576,-33.963332,-200.000000
AG::insert: -65.656566,-38.199391,-200.000000
            -65.656566,-38.199391,-200.000000
AG::insert: -55.555556,-42.291259,-200.000000
            -55.555556,-42.291259,-200.000000
AG::insert: -45.454545,-46.228502,-200.000000
            -45.454545,-46.228502,-200.000000
AG::insert: -35.353535,-50.001079,-200.000000
            -35.353535,-50.001079,-200.000000
AG::insert: -25.252525,-53.599370,-200.000000
            -25.252525,-53.599370,-200.000000
AG::insert: -15.151515,-57.014197,-200.000000
            -15.151515,-57.014197,-200.000000
AG::insert: -5.050505,-60.236853,-200.000000
            -5.050505,-60.236853,-200.000000
AG::insert: 5.050505,-63.259118,-200.000000
            5.050505,-63.259118,-200.000000
AG::insert: 15.151515,-66.073286,-200.000000
            15.151515,-66.073286,-200.000000
AG::insert: 25.252525,-68.672179,-200.000000
            25.252525,-68.672179,-200.000000
AG::insert: 35.353535,-71.049170,-200.000000
            35.353535,-71.049170,-200.000000
AG::insert: 45.454545,-73.198197,-200.000000
            45.454545,-73.198197,-200.000000
AG::insert: 55.555556,-75.113780,-200.000000
            55.555556,-75.113780,-200.000000
AG::insert: 65.656566,-76.791033,-200.000000
            65.656566,-76.791033,-200.000000
AG::insert: 75.757576,-78.225679,-200.000000
            75.757576,-78.225679,-200.000000
AG::insert: 85.858586,-79.414059,-200.000000
            85.858586,-79.414059,-200.000000
AG::insert: 95.959596,-80.353143,-200.000000
            95.959596,-80.353143,-200.000000
AG::insert: 106.060606,-81.040536,-200.000000
            106.060606,-81.040536,-200.000000
AG::insert: 116.161616,-81.474485,-200.000000
            116.161616,-81.474485,-200.000000
AG::insert: 126.262626,-81.653883,-200.000000
            126.262626,-81.653883,-200.000000
AG::insert: 136.363636,-81.578273,-200.000000
            136.363636,-81.578273,-200.000000
AG::insert: 146.464646,-81.247847,-200.000000
            146.464646,-81.247847,-200.000000
AG::insert: 156.565657,-80.663448,-200.000000
            156.565657,-80.663448,-200.000000
AG::insert: 166.666667,-79.826567,-200.000000
            166.666667,-79.826567,-200.000000
AG::insert: 176.767677,-78.739337,-200.000000
            176.767677,-78.739337,-200.000000
AG::insert: 186.868687,-77.404532,-200.000000
            186.868687,-77.404532,-200.000000
AG::insert: 196.969697,-75.825556,-200.000000
            196.969697,-75.825556,-200.000000
AG::insert: 207.070707,-74.006434,-200.000000
            207.070707,-74.006434,-200.000000
AG::insert: 217.171717,-71.951807,-200.000000
            217.171717,-71.951807,-200.000000
AG::insert: 227.272727,-69.666914,-200.000000
            227.272727,-69.666914,-200.000000
AG::insert: 237.373737,-67.157582,-200.000000
            237.373737,-67.157582,-200.000000
AG::insert: 247.474747,-64.430211,-200.000000
            247.474747,-64.430211,-200.000000
AG::insert: 257.575758,-61.491755,-200.000000
            257.575758,-61.491755,-200.000000
AG::insert: 267.676768,-58.349709,-200.000000
            267.676768,-58.349709,-200.000000
AG::insert: 277.777778,-55.012085,-200.000000
            277.777778,-55.012085,-200.000000
AG::insert: 287.878788,-51.487395,-200.000000
            287.878788,-51.487395,-200.000000
AG::insert: 297.979798,-47.784628,-200.000000
            297.979798,-47.784628,-200.000000
AG::insert: 308.080808,-43.913227,-200.000000
            308.080808,-43.913227,-200.000000
AG::insert: 318.181818,-39.883064,-200.000000
            318.181818,-39.883064,-200.000000
AG::insert: 328.282828,-35.704418,-200.000000
            328.282828,-35.704418,-200.000000
AG::insert: 338.383838,-31.387944,-200.000000
            338.383838,-31.387944,-200.000000
AG::insert: 348.484848,-26.944651,-200.000000
            348.484848,-26.944651,-200.000000
AG::insert: 358.585859,-22.385871,-200.000000
            358.585859,-22.385871,-200.000000
AG::insert: 368.686869,-17.723228,-200.000000
            368.686869,-17.723228,-200.000000
AG::insert: 378.787879,-12.968614,-200.000000
            378.787879,-12.968614,-200.000000
AG::insert: 388.888889,-8.134154,-200.000000
            388.888889,-8.134154,-200.000000
AG::insert: 398.989899,-3.232178,-200.000000
            398.989899,-3.232178,-200.000000
AG::insert: 409.090909,1.724815,-200.000000
            409.090909,1.724815,-200.000000
AG::insert: 419.191919,6.724182,-200.000000
            419.191919,6.724182,-200.000000
AG::insert: 429.292929,11.753174,-200.000000
            429.292929,11.753174,-200.000000
AG::insert: 439.393939,16.798966,-200.000000
            439.393939,16.798966,-200.000000
AG::insert: 449.494949,21.848690,-200.000000
            449.494949,21.848690,-200.000000
AG::insert: 459.595960,26.889469,-200.000000
            459.595960,26.889469,-200.000000
AG::insert: 469.696970,31.908446,-200.000000
            469.696970,31.908446,-200.000000
AG::insert: 479.797980,36.892823,-200.000000
            479.797980,36.892823,-200.000000
AG::insert: 489.898990,41.829889,-200.000000
            489.898990,41.829889,-200.000000
AG::insert: 500.000000,46.707052,-200.000000
            500.000000,46.707052,-200.000000
Done!

In [8]:
p2=paver.Paving(rings=[square],density=channel_scale)
p2.pave_all()  # might take a minute or two...


populate_dt: top
populate_dt: adding points
populate_dt: add constraints
populate_dt: end
populate_dt: top
populate_dt: adding points
populate_dt: 0/4
populate_dt: add constraints
populate_dt: 0/4
populate_dt: end
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 109 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 109 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 109 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 89 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 75 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 139 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 214 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 139 before alpha was reached
Discarding local node because boundary distance less than 25% longer than straight line
No more rings need paving!
Renumbering in preparation for statistics
------ Statistics ------
[None]
CPU user time (unix only):  55.08
CPU sys time  (unix only):  0.63
Wall time                :  55.7100000009
  cells:  1567
  edges:  2393
 points:  827
 cells/cpu-second:  28.4495279593
Relative scale error:      -0.262 ---   0.024+- 0.090  ---  0.531
Relative clearance error:  -0.793 ---   0.004+- 0.280  ---  0.911
 ----- ---------- ----- 
Done!

In [9]:
plt.figure(figsize=(6,6))
p2.plot()
plt.plot(channel[:,0],channel[:,1],'k-')
plt.legend(['"river"'])
plt.axis('equal')
plt.axis(xmin=-50,xmax=1050,ymin=-50,ymax=1050)


Out[9]:
(-50, 1050, -50, 1050)

In [10]:
plt.figure(figsize=(6,6))
plt.clf()
p2.density.to_grid(dx=20,dy=20,bounds=p2.bounds()).plot(interpolation='none')
plt.colorbar(label='Target edge length')


Out[10]:
<matplotlib.colorbar.Colorbar instance at 0x7f493bca0bd8>

Linear scale

The other way to describe variations in scale is linear interpolation. In the most basic form, a set of points are given, and each point has an associated scale. Standard linear interpolation (nearest neighbors in this case) then fills in the space between points.

To gain a bit of control over the interpolation, though, you can also specify polylines, and a scale at each vertex of each polyline. The triangulation used for interpolation will incorporate the polylines (a constrained delaunay triangulation). This can lead to fewer surprises when the polylines are sparse.

Unlike the telescoping field, with linearly interpolated fields it is up to the user to avoid overly sharp changes in scale. Following the 10% rule of thumb (adjacent cells shouldn't vary in length-scale by more than 10%), an absolute change in scale of $d$ takes a distance of $10d$. The key here is absolute change in scale - so whether it's interpolating from 1000 to 900 or 200 to 100, you still need $10(1000-900)=10(200-100)=1000$ units separation.


In [11]:
# Rectangular shoreline
rect=np.array([[0,0],[4000,0],[4000,1000],[0,1000]])

# The background, coarse scale, defined along a polyline
# surrounding the entire domain.
# linear scale does not support extrapolation - the shoreline
# must fall entirely within convex hull of the scale polylines.
bg=np.array([[-10,-10],[4010,-10],[4010,1010],[2000,1010]])
bg_scale=180*np.ones(len(bg))

# and one corner with higher resolution.
banks=np.array([[-10,800],[-10,1010],[200,1010]])
banks_scale=80*np.ones(len(banks))

lin_scale=constrained_delaunay.ConstrainedXYZField.from_polylines([bg,banks],
                                                                  [bg_scale,banks_scale])
p3=paver.Paving(rings=[rect],density=lin_scale)


populate_dt: top
populate_dt: adding points
populate_dt: add constraints
populate_dt: end
populate_dt: top
populate_dt: adding points
populate_dt: 0/4
populate_dt: add constraints
populate_dt: 0/4
populate_dt: end

In [12]:
# See how the interpolation actually distributed the scales
plt.figure(figsize=(15,4))
p3.density.to_grid(dx=50,dy=50).plot()
t=p3.density.tri()
plt.triplot(t,color='m')
plt.colorbar(label='Target edge length')
plt.title('Linearly interpolated scale: triangulation and result')


Building constrained Delaunay triangulation
Adding points
Adding constraints
Out[12]:
<matplotlib.text.Text at 0x7f493a061ed0>

In [13]:
p3.pave_all() # about 120s on reasonable desktop


!!!!WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 7 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 41 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 28 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 18 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 18 before alpha was reached
Discarding local node because boundary distance less than 25% longer than straight line
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 113 before alpha was reached
WARNING: while traversing the boundary in clear_boundary_range_by_alpha, encountered
  non-HINT node 47 before alpha was reached
!Discarding local node because boundary distance less than 25% longer than straight line
!No more rings need paving!
Renumbering in preparation for statistics
------ Statistics ------
[None]
CPU user time (unix only):  11.35
CPU sys time  (unix only):  0.15
Wall time                :  11.4900000021
  cells:  337
  edges:  538
 points:  202
 cells/cpu-second:  29.6916299559
Relative scale error:      -0.252 ---   0.013+- 0.067  ---  0.212
Relative clearance error:  -0.775 ---  -0.036+- 0.321  ---  0.757
 ----- ---------- ----- 
Done!

In [14]:
plt.figure(figsize=(15,4))
coll=p3.plot().set_color('g')
plt.axis('equal')
plt.axis(ymin=-50,ymax=1050,xmin=-50,xmax=4050)
plt.title('Resulting Mesh')


Out[14]:
<matplotlib.text.Text at 0x7f4939e794d0>