├── windmap.gif ├── windmap.mp4 ├── README.md ├── LICENSE ├── .gitignore └── windmap.py /windmap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rougier/windmap/HEAD/windmap.gif -------------------------------------------------------------------------------- /windmap.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rougier/windmap/HEAD/windmap.mp4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Wind map 3 | 4 | Animated streamlines using matplotlib. 5 | 6 | ![](windmap.gif) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, Nicolas P. Rougier 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /windmap.py: -------------------------------------------------------------------------------- 1 | import tqdm 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from matplotlib.animation import FuncAnimation, writers 5 | from matplotlib.collections import LineCollection 6 | 7 | class Streamlines(object): 8 | """ 9 | Copyright (c) 2011 Raymond Speth. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a 12 | copy of this software and associated documentation files (the "Software"), 13 | to deal in the Software without restriction, including without limitation 14 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | and/or sell copies of the Software, and to permit persons to whom the 16 | Software is furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | 29 | See: http://web.mit.edu/speth/Public/streamlines.py 30 | """ 31 | 32 | def __init__(self, X, Y, U, V, res=0.125, 33 | spacing=2, maxLen=2500, detectLoops=False): 34 | """ 35 | Compute a set of streamlines covering the given velocity field. 36 | 37 | X and Y - 1D or 2D (e.g. generated by np.meshgrid) arrays of the 38 | grid points. The mesh spacing is assumed to be uniform 39 | in each dimension. 40 | U and V - 2D arrays of the velocity field. 41 | res - Sets the distance between successive points in each 42 | streamline (same units as X and Y) 43 | spacing - Sets the minimum density of streamlines, in grid points. 44 | maxLen - The maximum length of an individual streamline segment. 45 | detectLoops - Determines whether an attempt is made to stop extending 46 | a given streamline before reaching maxLen points if 47 | it forms a closed loop or reaches a velocity node. 48 | 49 | Plots are generated with the 'plot' or 'plotArrows' methods. 50 | """ 51 | 52 | self.spacing = spacing 53 | self.detectLoops = detectLoops 54 | self.maxLen = maxLen 55 | self.res = res 56 | 57 | xa = np.asanyarray(X) 58 | ya = np.asanyarray(Y) 59 | self.x = xa if xa.ndim == 1 else xa[0] 60 | self.y = ya if ya.ndim == 1 else ya[:,0] 61 | self.u = U 62 | self.v = V 63 | self.dx = (self.x[-1]-self.x[0])/(self.x.size-1) # assume a regular grid 64 | self.dy = (self.y[-1]-self.y[0])/(self.y.size-1) # assume a regular grid 65 | self.dr = self.res * np.sqrt(self.dx * self.dy) 66 | 67 | # marker for which regions have contours 68 | self.used = np.zeros(self.u.shape, dtype=bool) 69 | self.used[0] = True 70 | self.used[-1] = True 71 | self.used[:,0] = True 72 | self.used[:,-1] = True 73 | 74 | # Don't try to compute streamlines in regions where there is no velocity data 75 | for i in range(self.x.size): 76 | for j in range(self.y.size): 77 | if self.u[j,i] == 0.0 and self.v[j,i] == 0.0: 78 | self.used[j,i] = True 79 | 80 | # Make the streamlines 81 | self.streamlines = [] 82 | while not self.used.all(): 83 | nz = np.transpose(np.logical_not(self.used).nonzero()) 84 | # Make a streamline starting at the first unrepresented grid point 85 | self.streamlines.append(self._makeStreamline(self.x[nz[0][1]], 86 | self.y[nz[0][0]])) 87 | 88 | 89 | def _interp(self, x, y): 90 | """ Compute the velocity at point (x,y) """ 91 | i = (x-self.x[0])/self.dx 92 | ai = i % 1 93 | 94 | j = (y-self.y[0])/self.dy 95 | aj = j % 1 96 | 97 | i, j = int(i), int(j) 98 | 99 | # Bilinear interpolation 100 | u = (self.u[j,i]*(1-ai)*(1-aj) + 101 | self.u[j,i+1]*ai*(1-aj) + 102 | self.u[j+1,i]*(1-ai)*aj + 103 | self.u[j+1,i+1]*ai*aj) 104 | 105 | v = (self.v[j,i]*(1-ai)*(1-aj) + 106 | self.v[j,i+1]*ai*(1-aj) + 107 | self.v[j+1,i]*(1-ai)*aj + 108 | self.v[j+1,i+1]*ai*aj) 109 | 110 | self.used[j:j+self.spacing,i:i+self.spacing] = True 111 | 112 | return u,v 113 | 114 | def _makeStreamline(self, x0, y0): 115 | """ 116 | Compute a streamline extending in both directions from the given point. 117 | """ 118 | 119 | sx, sy = self._makeHalfStreamline(x0, y0, 1) # forwards 120 | rx, ry = self._makeHalfStreamline(x0, y0, -1) # backwards 121 | 122 | rx.reverse() 123 | ry.reverse() 124 | 125 | return rx+[x0]+sx, ry+[y0]+sy 126 | 127 | def _makeHalfStreamline(self, x0, y0, sign): 128 | """ 129 | Compute a streamline extending in one direction from the given point. 130 | """ 131 | 132 | xmin = self.x[0] 133 | xmax = self.x[-1] 134 | ymin = self.y[0] 135 | ymax = self.y[-1] 136 | 137 | sx = [] 138 | sy = [] 139 | 140 | x = x0 141 | y = y0 142 | i = 0 143 | while xmin < x < xmax and ymin < y < ymax: 144 | u, v = self._interp(x, y) 145 | theta = np.arctan2(v,u) 146 | 147 | x += sign * self.dr * np.cos(theta) 148 | y += sign * self.dr * np.sin(theta) 149 | sx.append(x) 150 | sy.append(y) 151 | 152 | i += 1 153 | 154 | if self.detectLoops and i % 10 == 0 and self._detectLoop(sx, sy): 155 | break 156 | 157 | if i > self.maxLen / 2: 158 | break 159 | 160 | return sx, sy 161 | 162 | def _detectLoop(self, xVals, yVals): 163 | """ Detect closed loops and nodes in a streamline. """ 164 | x = xVals[-1] 165 | y = yVals[-1] 166 | D = np.array([np.hypot(x-xj, y-yj) 167 | for xj,yj in zip(xVals[:-1],yVals[:-1])]) 168 | return (D < 0.9 * self.dr).any() 169 | 170 | 171 | 172 | 173 | Y, X = np.mgrid[-3:3:100j, -3:3:100j] 174 | U, V = -1 - X**2 + Y, 1 + X - X*Y**2 175 | speed = np.sqrt(U*U + V*V) 176 | 177 | fig = plt.figure(figsize=(4,4)) 178 | ax = plt.subplot(1, 1, 1, aspect=1) 179 | 180 | 181 | lengths = [] 182 | colors = [] 183 | lines = [] 184 | 185 | s = Streamlines(X, Y, U, V) 186 | for streamline in s.streamlines: 187 | x, y = streamline 188 | points = np.array([x, y]).T.reshape(-1, 1, 2) 189 | segments = np.concatenate([points[:-1], points[1:]], axis=1) 190 | n = len(segments) 191 | 192 | D = np.sqrt(((points[1:] - points[:-1])**2).sum(axis=-1)) 193 | L = D.cumsum().reshape(n,1) + np.random.uniform(0,1) 194 | C = np.zeros((n,3)) 195 | C[:] = (L*1.5) % 1 196 | 197 | #linewidths = np.zeros(n) 198 | #linewidths[:] = 1.5 - ((L.reshape(n)*1.5) % 1) 199 | 200 | # line = LineCollection(segments, color=colors, linewidth=linewidths) 201 | line = LineCollection(segments, color=C, linewidth=0.5) 202 | lengths.append(L) 203 | colors.append(C) 204 | lines.append(line) 205 | 206 | ax.add_collection(line) 207 | 208 | def update(frame_no): 209 | for i in range(len(lines)): 210 | lengths[i] += 0.05 211 | colors[i][:] = (lengths[i]*1.5) % 1 212 | lines[i].set_color(colors[i]) 213 | pbar.update() 214 | 215 | ax.set_xlim(-3,+3), ax.set_xticks([]) 216 | ax.set_ylim(-3,+3), ax.set_yticks([]) 217 | plt.tight_layout() 218 | 219 | n = 27 220 | # animation = FuncAnimation(fig, update, interval=10) 221 | animation = FuncAnimation(fig, update, frames=n, interval=20) 222 | pbar = tqdm.tqdm(total=n) 223 | # animation.save('wind.mp4', writer='ffmpeg', fps=60) 224 | animation.save('wind.gif', writer='imagemagick', fps=30) 225 | pbar.close() 226 | plt.show() 227 | 228 | --------------------------------------------------------------------------------