├── .buildinfo ├── .gitignore ├── .nojekyll ├── LICENSE ├── README.md ├── fvm_classes.py └── fvm_examples.py /.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: e691757387943e9686969a31d5623e7e 4 | tags: a205e9ed8462ae86fdd2f73488852ba9 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | Icon? 37 | ehthumbs.db 38 | Thumbs.db -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danieljfarrell/FVM/f2da14c77d16dceca33c5e9ac68b58986df64797/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Daniel Farrell 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 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. 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 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4647887.svg)](https://doi.org/10.5281/zenodo.4647887) 2 | 3 | Solving the advection-diffusion equation on an non-uniform mesh with the finite-volume method 4 | ********************************************************************************************* 5 | 6 | 7 | This repo is basically my notes on learning the finite-volume method when applied to the advection-diffusion equation. The methods are based on the the book by W. Hundsdorfer and J. G. Verwer, [Numerical solutions of time-dependent advection-diffusion reaction equations](https://books.google.co.uk/books?isbn=3540034404). 8 | 9 | For my lab-book notes on solving the advection-diffusion equation with the finite-volume method see the accompanying gh-pages branch, [https://danieljfarrell.github.io/FVM/index.html](https://danieljfarrell.github.io/FVM/index.html). 10 | 11 | The code is written in python using numpy as scipy libraries. 12 | 13 | Ideas I wanted to explore: 14 | 15 | * How to implement a cell centred mesh? 16 | * How to implement a non-uniform mesh? 17 | * How to include adaptive upwinding? 18 | - This means that the discretisation automatically adjusts to an appropriate scheme depending on the local value of the Peclet number. 19 | - I use exponentially fitting which is a nice method because it enables to equations to be solved for *any* Peclet number. 20 | - A central difference scheme is limited to Peclet number < 2, however a upwind scheme the Peclet number is unbounded. 21 | - Please not that the CFL condition imposes a limited on the time step when using upwind method. 22 | * How to include Robin boundary conditions which do not allow any out flow. 23 | * How to include Dirichlet boundary conditions. 24 | * How to export a movie of time-dependent simulations. 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /fvm_classes.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import collections 3 | import numpy as np 4 | from scipy import sparse 5 | from scipy.sparse import linalg 6 | from scipy.sparse import dia_matrix 7 | np.random.seed(seed=1) 8 | 9 | # Supporting functions 10 | gaussian = lambda z, height, position, hwhm: height * np.exp(-np.log(2) * ((z - position)/hwhm)**2) 11 | H = lambda z: 0.5 * (1 - np.sign(z)) 12 | TH = lambda x, sigma, mu: np.where( x>(mu-sigma), 1, 0) * np.where(x<(mu+sigma), 1, 0) 13 | 14 | def check_index_within_bounds(i, min_i, max_i): 15 | """Checks that the index specified (can be number or an iterable) is within the given range.""" 16 | success = np.all((i>=min_i)*(i<=max_i)) 17 | if success: 18 | return True 19 | 20 | if isinstance(i, collections.Iterable): 21 | # The index is array-like 22 | print "Index is out of bounds.\ni=%s" % i[np.where(np.logical_not((i>=min_i)*(i<=max_i)))] 23 | else: 24 | # The index is an number 25 | print "Index is out of bounds.\ni=%s" % i 26 | return False 27 | 28 | 29 | class Mesh(object): 30 | """A 1D cell centered mesh defined by faces for the finite volume method.""" 31 | 32 | def __init__(self, faces): 33 | super(Mesh, self).__init__() 34 | 35 | # Check for duplicated points 36 | if len(faces) != len(set(faces)): 37 | raise ValueError("The faces array contains duplicated positions. No cell can have zero volume so please update with unique face positions.") 38 | self.faces = np.array(faces) 39 | self.cells = 0.5 * (self.faces[0:-1] + self.faces[1:]) 40 | self.J = len(self.cells) 41 | self.cell_widths = (self.faces[1:] - self.faces[0:-1]) 42 | 43 | def h(self, i): 44 | """Returns the width of the cell at the specified index.""" 45 | return self.cell_widths[i] 46 | 47 | def hm(self, i): 48 | """Distance between centroids in the backwards direction.""" 49 | if not check_index_within_bounds(i,1,self.J-1): 50 | raise ValueError("hm index runs out of bounds") 51 | return (self.cells[i] - self.cells[i-1]) 52 | 53 | def hp(self, i): 54 | """Distance between centroids in the forward direction.""" 55 | if not check_index_within_bounds(i,0,self.J-2): 56 | raise ValueError("hp index runs out of bounds") 57 | return (self.cells[i+1] - self.cells[i]) 58 | 59 | class CellVariable(np.ndarray): 60 | """Representation of a variable defined at the cell centers. Provides interpolation functions to calculate the value at cell faces.""" 61 | 62 | # http://docs.scipy.org/doc/numpy/user/basics.subclassing.html 63 | def __new__(cls, input_array, mesh=None): 64 | 65 | # If `input_array` is actually just a constant 66 | # convert it to an array of len the number of cells. 67 | try: 68 | len(input_array) 69 | except: 70 | input_array = input_array*np.ones(len(mesh.cells)) 71 | 72 | obj = np.asarray(input_array).view(cls) 73 | obj.mesh = mesh 74 | return obj 75 | 76 | def __array_finalize__(self, obj): 77 | if obj is None: return 78 | self.mesh = getattr(obj, 'mesh', None) 79 | self.__get_items__ = getattr(obj, '__get_items__', None) 80 | 81 | def m(self, i): 82 | """Linear interpolation of the cell value at the right hand face i.e. along the _m_inus direction.""" 83 | return self.mesh.h(i)/(2*self.mesh.hm(i))*self[i-1] + self.mesh.h(i-1)/(2*self.mesh.hm(i))*self[i] 84 | 85 | def p(self, i): 86 | """Linear interpolation of the cell value at the right hand face i.e. along the _p_lus direction.""" 87 | return self.mesh.h(i+1)/(2*self.mesh.hp(i))*self[i] + self.mesh.h(i)/(2*self.mesh.hp(i))*self[i+1] 88 | 89 | class AdvectionDiffusionModel(object): 90 | 91 | """A model for the advection-diffusion equation""" 92 | def __init__(self, faces, a, d, k, discretisation="central"): 93 | super(AdvectionDiffusionModel, self).__init__() 94 | 95 | self.mesh = Mesh(faces) 96 | self.a = CellVariable(a, mesh=self.mesh) 97 | self.d = CellVariable(d, mesh=self.mesh) 98 | self.k = k 99 | self.discretisation = discretisation 100 | 101 | # Check Peclet number 102 | import warnings 103 | mu = self.peclet_number() 104 | if np.max(np.abs(mu)) >= 1.5 and np.max(np.abs(mu)) < 2.0: 105 | warnings.warn("\n\nThe Peclet number is %g, this is getting close to the limit of mod 2.") 106 | elif np.max(np.abs(mu)) > 2: 107 | warnings.warn("\n\nThe Peclet number (%g) has exceeded the maximum value of mod 2 for the central discretisation scheme." % (np.max(mu),) ) 108 | 109 | # Check CFL condition 110 | CFL = self.CFL_condition() 111 | if np.max(np.abs(CFL)) > 0.5 and np.max(np.abs(CFL)) < 1.0: 112 | warnings.warn("\n\nThe CFL condition value is %g, it is getting close to the upper limit." % (np.max(CFL),) ) 113 | elif np.max(np.abs(CFL)) > 1: 114 | warnings.warn("\n\nThe CFL condition value is %g, and has gone above the upper limit." % (np.max(CFL),) ) 115 | 116 | if discretisation == "exponential": 117 | self.kappa = (np.exp(mu) + 1)/(np.exp(mu) - 1) - 2/mu; 118 | self.kappa[np.where(mu==0.0)] = 0 119 | self.kappa[np.where(np.isposinf(mu))] = 1 120 | self.kappa[np.where(np.isneginf(mu))] = -1 121 | elif discretisation == "upwind": 122 | kappa_neg = np.where(self.a<0,-1,0) 123 | kappa_pos = np.where(self.a>0,1,0) 124 | self.kappa = kappa_neg + kappa_pos 125 | elif discretisation == "central": 126 | self.kappa = np.zeros(self.mesh.J) 127 | else: 128 | print "Please set `discretisation` to one of the following: `upwind`, `central` or `exponential`." 129 | 130 | # Artificially modify the diffusion coefficient to introduce adpative discretisation 131 | self.d = self.d + 0.5 * self.a * self.mesh.cell_widths * self.kappa 132 | print "Using kappa", np.min(self.kappa), np.max(self.kappa) 133 | print self.kappa 134 | 135 | def peclet_number(self): 136 | return self.a * self.mesh.cell_widths / self.d 137 | 138 | def CFL_condition(self): 139 | return self.a * self.k / self.mesh.cell_widths 140 | 141 | def set_boundary_conditions(self, left_flux=None, right_flux=None, left_value=None, right_value=None ): 142 | """Make sure this function is used sensibly otherwise the matrix will be ill posed.""" 143 | 144 | self.left_flux = left_flux 145 | self.right_flux = right_flux 146 | self.left_value = left_value 147 | self.right_value = right_value 148 | 149 | def _interior_matrix_elements(self, i): 150 | # Interior coefficients for matrix equation 151 | ra = lambda i, a, d, m: 1./m.h(i)*(a.m(i)*m.h(i)/(2*m.hm(i)) + d.m(i)/m.hm(i)) 152 | rb = lambda i, a, d, m: 1./m.h(i)*(a.m(i)*m.h(i-1)/(2*m.hm(i)) - a.p(i)*m.h(i+1)/(2*m.hp(i)) - d.m(i)/m.hm(i) - d.p(i)/m.hp(i)) 153 | rc = lambda i, a, d, m: 1./m.h(i)*(-a.p(i)*m.h(i)/(2*m.hp(i)) + d.p(i)/m.hp(i)) 154 | return ra(i, self.a, self.d, self.mesh), rb(i, self.a, self.d, self.mesh), rc(i,self.a, self.d, self.mesh) 155 | 156 | def _robin_boundary_condition_matrix_elements_left(self): 157 | # Left hand side Robin boundary coefficients for matrix equation 158 | b1 = lambda a, d, m: 1./m.h(0)*(-a.p(0)*m.h(1)/(2*m.hp(0)) - d.p(0)/m.hp(0) ) 159 | c1 = lambda a, d, m: 1./m.h(0)*(-a.p(0)*m.h(0)/(2*m.hp(0)) + d.p(0)/m.hp(0) ) 160 | 161 | # Index and element value 162 | locations = [(0,0), (0,1)] 163 | values = ( b1(self.a, self.d, self.mesh ), 164 | c1(self.a, self.d, self.mesh ) ) 165 | return tuple([list(x) for x in zip(locations, values)]) 166 | 167 | def _robin_boundary_condition_matrix_elements_right(self, matrix=None): 168 | # Right hand side Robin boundary coefficients for matrix equation 169 | aJ = lambda a, d, m: 1./m.h(m.J-1)*( a.m(m.J-1)*m.h(m.J-1)/(2*m.hm(m.J-1)) + d.m(m.J-1)/m.hm(m.J-1) ) 170 | bJ = lambda a, d, m: 1./m.h(m.J-1)*( a.m(m.J-1)*m.h(m.J-2)/(2*m.hm(m.J-1)) - d.m(m.J-1)/m.hm(m.J-1) ) 171 | J = self.mesh.J # Index and element value 172 | 173 | # Index and element value 174 | locations = [(J-1,J-2), (J-1,J-1)] 175 | values = ( aJ(self.a, self.d, self.mesh ), 176 | bJ(self.a, self.d, self.mesh ) ) 177 | return tuple([list(x) for x in zip(locations, values)]) 178 | 179 | def _robin_boundary_condition_vector_elements_left(self): 180 | # Index and boundary condition vector elements for Robin conditions 181 | location = [0] 182 | value = [self.left_flux/self.mesh.h(0)] 183 | return tuple([list(x) for x in zip(location, value)]) 184 | 185 | def _robin_boundary_condition_vector_elements_right(self): 186 | # Index and boundary condition vector elements for Robin conditions 187 | location = [self.mesh.J-1] 188 | value = [-self.right_flux/self.mesh.h(self.mesh.J-1)] 189 | return tuple([list(x) for x in zip(location, value)]) 190 | 191 | def _dirichlet_boundary_condition_matrix_elements_left(self): 192 | # Left hand side Robin boundary coefficients for matrix equation 193 | rb = lambda i, a, d, m: 1./m.h(i)*(a.m(i)*m.h(i-1)/(2*m.hm(i)) - a.p(i)*m.h(i+1)/(2*m.hp(i)) - d.m(i)/m.hm(i) - d.p(i)/m.hp(i)) 194 | rc = lambda i, a, d, m: 1./m.h(i)*(-a.p(i)*m.h(i)/(2*m.hp(i)) + d.p(i)/m.hp(i)) 195 | 196 | # Index and element value 197 | locations = [(0,0), (0,1)] 198 | # values = ( rb(0, self.a, self.d, self.mesh ), 199 | # rc(0, self.a, self.d, self.mesh ) ) 200 | values = ( 0, 201 | 1 ) 202 | return tuple([list(x) for x in zip(locations, values)]) 203 | 204 | def _dirichlet_boundary_condition_matrix_elements_right(self): 205 | # Right hand side Robin boundary coefficients for matrix equation 206 | ra = lambda i, a, d, m: 1./m.h(i)*(a.m(i)*m.h(i)/(2*m.hm(i)) + d.m(i)/m.hm(i)) 207 | rb = lambda i, a, d, m: 1./m.h(i)*(a.m(i)*m.h(i-1)/(2*m.hm(i)) - a.p(i)*m.h(i+1)/(2*m.hp(i)) - d.m(i)/m.hm(i) - d.p(i)/m.hp(i)) 208 | J = self.mesh.J # Index and element value 209 | 210 | # Index and element value 211 | locations = [(J-1,J-2), (J-1,J-1)] 212 | # values = ( ra(self.J-1, self.a, self.d, self.mesh ), 213 | # rb(self.J-1, self.a, self.d, self.mesh ) ) 214 | values = ( 0, 215 | 1 ) 216 | return tuple([list(x) for x in zip(locations, values)]) 217 | 218 | 219 | def _dirichlet_boundary_condition_vector_elements_left(self): 220 | # Index and boundary condition vector elements for Dirichlet conditions 221 | # NB these are always zero, unless BCs are time varying 222 | location = [0] 223 | value = [0] 224 | return tuple([list(x) for x in zip(location, value)]) 225 | 226 | def _dirichlet_boundary_condition_vector_elements_right(self): 227 | # Index and boundary condition vector elements for Dirichlet conditions 228 | # NB these are always zero, unless BCs are time varying 229 | location = [self.mesh.J-1] 230 | value = [0] 231 | return tuple([list(x) for x in zip(location, value)]) 232 | 233 | def alpha_matrix(self): 234 | """The alpha matrix is used to mask boundary conditions values for Dirichlet 235 | conditions. Otherwise for a fully Neumann (or Robin) system it is equal to 236 | the identity matrix.""" 237 | a1 = 0 if self.left_flux is None else 1 238 | aJ = 0 if self.left_flux is None else 1 239 | diagonals = np.ones(self.mesh.J) 240 | diagonals[0] = a1 241 | diagonals[-1] = aJ 242 | return sparse.diags(diagonals, 0) 243 | 244 | def beta_vector(self): 245 | """Returns the robin boundary condition vector.""" 246 | b = np.zeros(self.mesh.J) 247 | 248 | if self.left_flux is not None: 249 | left_bc_elements = self._robin_boundary_condition_vector_elements_left() 250 | 251 | if self.right_flux is not None: 252 | right_bc_elements = self._robin_boundary_condition_vector_elements_right() 253 | 254 | if self.left_value is not None: 255 | left_bc_elements = self._dirichlet_boundary_condition_vector_elements_left() 256 | 257 | if self.right_value is not None: 258 | right_bc_elements = self._dirichlet_boundary_condition_vector_elements_right() 259 | 260 | bcs = left_bc_elements + right_bc_elements 261 | for inx, value in bcs: 262 | b[inx] = value 263 | return b 264 | 265 | def coefficient_matrix(self): 266 | """Returns the coefficient matrix which appears on the left hand side.""" 267 | J = self.mesh.J 268 | k = self.k 269 | m = self.mesh 270 | a = self.a 271 | d = self.d 272 | 273 | padding = np.array([0]) # A element which is pushed off the edge of the matrix by the spdiags function 274 | zero = padding # Yes, its the same. But this element is included in the matrix (semantic difference). 275 | one = np.array([1]) # 276 | 277 | if self.left_flux is not None: 278 | left_bc_elements = self._robin_boundary_condition_matrix_elements_left() 279 | 280 | if self.right_flux is not None: 281 | right_bc_elements = self._robin_boundary_condition_matrix_elements_right() 282 | 283 | if self.left_value is not None: 284 | left_bc_elements = self._dirichlet_boundary_condition_matrix_elements_left() 285 | 286 | if self.right_value is not None: 287 | right_bc_elements = self._dirichlet_boundary_condition_matrix_elements_right() 288 | 289 | # Use the functions to layout the matrix Note that the boundary 290 | # condition elements are set to zero, they are filled in as 291 | # the next step. 292 | inx = np.array(range(1,J-1)) 293 | ra, rb, rc = self._interior_matrix_elements(inx) 294 | # c1 295 | upper = np.concatenate([padding, zero, rc ]) 296 | 297 | # b1 bJ 298 | central = np.concatenate([zero, rb, zero ]) 299 | 300 | # aJ 301 | lower = np.concatenate([ra, zero , padding]) 302 | 303 | A = sparse.spdiags([lower, central, upper], [-1,0,1], J, J).todok() 304 | 305 | # Apply boundary conditions elements 306 | bcs = left_bc_elements + right_bc_elements 307 | for inx, value in bcs: 308 | print inx, value 309 | A[inx] = value 310 | return dia_matrix(A) 311 | 312 | 313 | if __name__ == '__main__': 314 | 315 | def geo_series(n, r, min_spacing=0.01): 316 | total = 0 317 | series = [] 318 | for i in range(n): 319 | 320 | if i == 0: 321 | total = 1 322 | else: 323 | total = total - total*r 324 | series.append(total) 325 | 326 | series = np.array(series) 327 | norm = series / (np.max(series) - np.min(series)) 328 | series = norm - np.min(norm) 329 | series = np.abs(series - 1) 330 | series_diff = np.gradient(series) 331 | inx = np.where(series_diff > min_spacing) 332 | print inx 333 | series_diff[inx] = min_spacing 334 | series_reconstruct = np.cumsum(series_diff) 335 | if np.min(series_reconstruct) != 0.0: 336 | series_reconstruct = np.array([0] + series_reconstruct.tolist()) 337 | if np.max(series_reconstruct) != 1.0: 338 | series_reconstruct = np.array(series_reconstruct.tolist() + [1]) 339 | return series_reconstruct 340 | 341 | #faces = geo_series(200, 0.15) 342 | #print faces.shape, faces 343 | 344 | #faces = np.concatenate((np.array([-0.5]), np.sort(np.random.uniform(-0.5, 1, 50)), np.array([1]))) 345 | #faces = np.linspace(0, 1, 50) 346 | faces = np.concatenate([np.linspace(0, 0.99, 50), np.logspace(np.log10(0.991), np.log10(1.0), 100)]) 347 | mesh = Mesh(faces) 348 | 349 | a = CellVariable(1, mesh=mesh) # Advection velocity 350 | d = CellVariable(1e-3, mesh=mesh) # Diffusion coefficient 351 | k = 0.01 # Time step 352 | theta = 1.0 353 | left_value = 1.0 354 | #left_flux = 0.0 355 | right_flux = 0.0 356 | 357 | # Initial conditions 358 | w_init = 0.5*TH(mesh.cells, 0.4, 0) 359 | w_init = np.sin(np.pi*mesh.cells)**100 360 | w_init[0] = left_value 361 | #w_init[0] = left_flux 362 | 363 | # Source term 364 | #s[int(np.median(range(mesh.J)))] = 0.0 365 | 366 | 367 | model = AdvectionDiffusionModel(faces, a, d, k, discretisation="exponential") 368 | model.set_boundary_conditions(left_value=1., right_value=0.) 369 | #model.set_boundary_conditions(left_flux=left_flux, right_flux=left_flux) 370 | M = model.coefficient_matrix() 371 | alpha = model.alpha_matrix() 372 | beta = model.beta_vector() 373 | I = sparse.identity(model.mesh.J) 374 | 375 | # Construct linear system from discretised matrices, A.x = d 376 | A = I - k*theta*alpha*M 377 | d = (I + k*(1-theta)*alpha*M)*w_init + beta 378 | 379 | print "Peclet number", np.min(model.peclet_number()), np.max(model.peclet_number()) 380 | print "CFL condition", np.min(model.CFL_condition()), np.max(model.CFL_condition()) 381 | 382 | # matplotlib for movie export 383 | # see, http://matplotlib.org/examples/animation/moviewriter.html 384 | import matplotlib 385 | matplotlib.use("Agg") 386 | import matplotlib.pyplot as plt 387 | import matplotlib.animation as manimation 388 | print manimation.writers.__dict__ 389 | FFMpegWriter = manimation.writers['ffmpeg'] 390 | metadata = dict(title='Movie Test', artist='Matplotlib', comment='Movie support!') 391 | writer = FFMpegWriter(fps=15, metadata=metadata) 392 | 393 | fig = plt.figure() 394 | l0, = plt.plot([],[], 'r-', lw=1) 395 | l1, = plt.plot([],[], 'k-o', markersize=4) 396 | plt.xlim(np.min(faces), np.max(faces)) 397 | plt.ylim(0,1.2) 398 | l1.set_data(mesh.cells,w_init) 399 | 400 | # # Analytical solution for Dirichlet boundary conditions 401 | analytical_x = np.concatenate([np.array([np.min(faces)]), mesh.cells, np.array([np.max(faces)])]) 402 | analytical_solution = np.concatenate([np.array([model.left_value]), (np.exp(a/d) - np.exp(mesh.cells*a/d))/(np.exp(a/d)-1), np.array([model.right_value]) ]) 403 | #analytical_solution2 = np.concatenate([np.array([model.left_value]), (np.exp(a/model.d) - np.exp(mesh.cells*a/model.d))/(np.exp(a/model.d)-1), np.array([model.right_value]) ]) 404 | 405 | w = w_init 406 | with writer.saving(fig, "fvm_advection_diffusion_1.mp4", 300): 407 | 408 | for i in range(201): 409 | #w = linalg.spsolve(A.tocsc(), M * w + s) 410 | d = (I + k*(1-theta)*alpha*M)*w + beta 411 | w = linalg.spsolve(A, d) 412 | 413 | if i == 0: 414 | l1.set_data(mesh.cells,w_init) 415 | writer.grab_frame() 416 | 417 | if i % 1 == 0 or i == 0: 418 | l1.set_data(mesh.cells,w) 419 | #l0.set_data(analytical_x, analytical_solution) 420 | area = np.sum(w * mesh.cell_widths) 421 | print "#%d; t=%g; area=%g:" % (i, i*k,area) 422 | writer.grab_frame() 423 | 424 | 425 | 426 | -------------------------------------------------------------------------------- /fvm_examples.py: -------------------------------------------------------------------------------- 1 | from fvm_classes import * 2 | import numpy as np 3 | from scipy.sparse import linalg 4 | from clint.textui import progress 5 | 6 | # matplotlib for movie export here 7 | # http://matplotlib.org/examples/animation/moviewriter.html 8 | 9 | comment = "In all these example numerical instabilities are exist, this is to illustrate the\n\ 10 | difference between second order central difference, first order upwind and exponentially\n\ 11 | fitted discretisation schemes. The Peclet number is around 20 which means that central\n\ 12 | discretisation are numerically unstable (they are only stable with Peclet number below\n\ 13 | 2). Upwind scheme do not have any restriction on Peclet number for stability, however\n\ 14 | they introduce numerical (artificial) diffusion to the solution. Exponentially fitting\n\ 15 | guarantees stability (with regard to Peclet number) because it is a hybrid\n\ 16 | discretisation scheme which combines an weighted averages the central and upwind\n\ 17 | approaches. The the Peclet number is large the discretisation is weighted in favor of\n\ 18 | the upwind scheme, and when the Peclet number is low a central scheme is favored." 19 | 20 | 21 | def hundsdorfer_examples(faces, export_filename="movie.mp4"): 22 | 23 | a = 1 # Advection velocity 24 | d = 1e-3 # Diffusion coefficient 25 | k = 0.01 # Time step 26 | 27 | log = "Comparison of central, upwind and exponential fitting schemes." 28 | print log 29 | central = Model(faces, a, d, k, discretisation="central") 30 | central.set_boundary_conditions(left_value=1., right_value=0.) 31 | A1 = central.A_matrix() 32 | M1 = central.M_matrix() 33 | b1 = central.b_vector() 34 | 35 | upwind = Model(faces, a, d, k, discretisation="upwind") 36 | upwind.set_boundary_conditions(left_value=1., right_value=0.) 37 | A2 = upwind.A_matrix() 38 | M2 = upwind.M_matrix() 39 | b2 = upwind.b_vector() 40 | 41 | exponential = Model(faces, a, d, k, discretisation="exponential") 42 | exponential.set_boundary_conditions(left_value=1., right_value=0.) 43 | A3 = exponential.A_matrix() 44 | M3 = exponential.M_matrix() 45 | b3 = exponential.b_vector() 46 | 47 | import matplotlib 48 | matplotlib.use("Agg") 49 | import matplotlib.pyplot as plt 50 | import matplotlib.animation as manimation 51 | print manimation.writers.__dict__ 52 | FFMpegWriter = manimation.writers['ffmpeg'] 53 | metadata = dict(title=log, artist='https://github.com/danieljfarrell/FVM', comment=comment) 54 | writer = FFMpegWriter(fps=24, metadata=metadata) 55 | 56 | fig = plt.figure() 57 | l0, = plt.plot([],[], 'k-', lw=1) 58 | l_central, = plt.plot([],[], 'r-o', label="central", markersize=6, alpha=0.5) 59 | l_upwind, = plt.plot([],[], 'g-o', label="upwind", markersize=6, alpha=0.5) 60 | l_exp, = plt.plot([],[], 'b-o', label="exponential", markersize=6, alpha=0.5) 61 | plt.xlim(-0.1, 1.1) 62 | plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) 63 | plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.05), fancybox=True, shadow=True, ncol=3) 64 | plt.ylim(-0.2,1.2) 65 | 66 | # # Analytical solution for Dirichlet boundary conditions 67 | mesh = central.mesh 68 | analytical_x = np.linspace(0,1,2000) 69 | import sympy 70 | a_s, h_s, d_s= sympy.var("a h d") 71 | f = (sympy.exp(a_s/d_s) - sympy.exp(a_s*h_s/d_s))/(sympy.exp(a_s/d_s)-1) 72 | analytical_solution = [f.subs({a_s:a, d_s:d, h_s:x}) for x in analytical_x ] 73 | analytical_solution = np.array(analytical_solution) 74 | 75 | w_init = np.sin(np.pi*mesh.cells)**100 76 | w1 = w_init 77 | w2 = w_init 78 | w3 = w_init 79 | 80 | #exit(1) 81 | with writer.saving(fig, export_filename, 300): 82 | 83 | iters = 251 84 | for i in progress.bar(range(iters)): 85 | w1 = linalg.spsolve(A1.tocsc(), M1 * w1 + b1) 86 | w2 = linalg.spsolve(A2.tocsc(), M2 * w2 + b2) 87 | w3 = linalg.spsolve(A3.tocsc(), M3 * w3 + b3) 88 | 89 | if i == 0: 90 | l_central.set_data(mesh.cells, w_init) 91 | l_upwind.set_data(mesh.cells, w_init) 92 | l_exp.set_data(mesh.cells, w_init) 93 | l0.set_data(analytical_x, analytical_solution) 94 | writer.grab_frame() 95 | 96 | if i % 1 == 0 or i == 0: 97 | l_central.set_data(mesh.cells, w1) 98 | l_upwind.set_data(mesh.cells, w2) 99 | l_exp.set_data(mesh.cells, w3) 100 | l0.set_data(analytical_x, analytical_solution) 101 | writer.grab_frame() 102 | 103 | if __name__ == '__main__': 104 | 105 | hundsdorfer_examples(np.linspace(0, 1, 50), export_filename="uniform_grid.mp4") 106 | faces = np.concatenate((np.array([0]), np.sort(np.random.uniform(0, 1, 48)), np.array([1]))) 107 | hundsdorfer_examples(faces, export_filename="random_grid.mp4") 108 | def geo_series(n, r): 109 | total = 0 110 | series = [] 111 | for i in range(n): 112 | 113 | if i == 0: 114 | total = 1 115 | else: 116 | total = total - total*r 117 | series.append(total) 118 | 119 | series = np.array(series) 120 | norm = series / (np.max(series) - np.min(series)) 121 | series = norm - np.min(norm) 122 | return np.abs(series - 1) 123 | 124 | faces = geo_series(50, 0.15) 125 | hundsdorfer_examples(faces, export_filename="geometric_grid.mp4") 126 | --------------------------------------------------------------------------------