diff --git a/docs/conf.py b/docs/conf.py index 3ac8a2f32..818832e68 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,8 +45,12 @@ "numpydoc", "nbsphinx", "sphinx_gallery.gen_gallery", + "sphinxcontrib.bibtex" ] +# Bibtex file location for references +bibtex_bibfiles = ['./content/references.bib'] + # Autosummary pages will be generated by sphinx-autogen instead of sphinx-build autosummary_generate = True diff --git a/docs/content/finite_volume.rst b/docs/content/finite_volume.rst deleted file mode 100644 index 3703dd5cc..000000000 --- a/docs/content/finite_volume.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _api_FiniteVolume: - -Finite Volume -************* - -Any numerical implementation requires the discretization of continuous -functions into discrete approximations. These approximations are typically -organized in a mesh, which defines boundaries, locations, and connectivity. Of -specific interest to geophysical simulations, we require that averaging, -interpolation and differential operators be defined for any mesh. In SimPEG, -we have implemented a staggered mimetic finite volume approach (`Hyman and -Shashkov, 1999 `_). This -approach requires the definitions of variables at either cell-centers, nodes, -faces, or edges as seen in the figure below. - -.. image:: ../images/finitevolrealestate.png - :width: 400 px - :alt: FiniteVolume - :align: center diff --git a/docs/content/finite_volume_index.rst b/docs/content/finite_volume_index.rst new file mode 100644 index 000000000..5895ad802 --- /dev/null +++ b/docs/content/finite_volume_index.rst @@ -0,0 +1,125 @@ +.. _finite_volume_index: + +Intoduction to Finite Volume +**************************** + +What is Finite Volume? +---------------------- + +The finite volume method is a method for numerically approximating the solution to partial differential equations. +Implementation of the finite volume method requires the discretization of continuous functions and variables. +Discrete representations of functions and variables are organized on a numerical grid (or mesh). +The final product of the approach is a linear system of equations :math:`\boldsymbol{A \phi=q}` +that can be solved to compute the discrete approximation of a desired quantity. + +.. figure:: ../images/finitevolumeschematic.png + :width: 700 + :align: center + + Conceptual illustrating for solving PDEs with the finite volume method. + +In *discretize*, we use a staggered mimetic finite volume approach (:cite:`haber2014,HymanShashkov1999`). +This approach requires the definitions of variables at either cell-centers, nodes, faces, or edges. +This method is different from finite difference methods, +as the final linear system is constructed by approximating the inner products between +test functions and partial differential equations. + +**Contents:** + + - :ref:`Meshes ` + - :ref:`Interpolation, Averaging and Differential Operators ` + - :ref:`Inner Products ` + - :ref:`Discretizing PDEs Derivation Examples ` + +**Tutorials and Examples Gallery:** + + - :ref:`Mesh Generation ` + - :ref:`Interpolation, Averaging and Differential Operators ` + - :ref:`Inner Products ` + - :ref:`Discretizing PDEs Derivation Examples ` + - :ref:`Examples Gallery ` + + +Examples +-------- + +Below are several examples of the final linear system obtained using the finite volume approach. +A comprehensive derivation of the final result is not provided here. The full derivations are +provide in the :ref:`discretizing PDEs derivation examples ` theory section. + +Direct Current Resistivity +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The governing equation for the direct current resistivity problem is given by: + +.. math:: + \nabla \cdot \sigma \nabla \phi = -q_s + +where + + - :math:`\phi` is the electric potential + - :math:`\sigma` is the electrical conductivity within the domain + - :math:`q_s` is a general representation of the source term + - :math:`\nabla` is the gradient operator + - :math:`\nabla \cdot` is the divergence operator + +If we choose to define the discrete representation of the electric potential on the nodes, +the solution for the electric potentials after applying the finite volume approach is given by: + +.. math:: + \boldsymbol{[G^T \! M_{\sigma e} G ]} \boldsymbol{\phi} = \mathbf{q_s} + +where :math:`\boldsymbol{G^T \! M_{\sigma e} G }` is a sparse matrix and + + - :math:`\boldsymbol{\phi}` is the discrete approximation to the electric potentials on the nodes + - :math:`\boldsymbol{G}` is the :ref:`discrete gradient operator ` + - :math:`\boldsymbol{M_{\sigma e}}` is the :ref:`mass matrix for electrical conductivity ` + - :math:`\boldsymbol{q_s}` is the discrete representation of the source term on the nodes + + +Frequency Domain Electromagnetics +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The governing equations for the frequency domain electromagnetic problems, +for a source current, can be expressed using Maxwell's equations: + +.. math:: + \begin{align} + &\nabla \times \mu^{-1} \vec{B} - \sigma \vec{E} = \vec{J}_s \\ + &\nabla \times \vec{E} = - i\omega \vec{B} + \end{align} + +where + + - :math:`\vec{E}` is the electric field + - :math:`\vec{B}` is the magnetic flux density + - :math:`\vec{J}_s` is a general representation of the source term + - :math:`\sigma` is the electrical conductivity within the domain + - :math:`\mu` is the magnetic permeability within the domain + - :math:`\omega` is the angular frequency + - :math:`\nabla \times` is the curl operator + +Here we choose to define the discrete representation of the electric field on edges +and the discrete representation of the magnetic flux density on faces. +The solution for the electric potentials after applying the finite volume approach is given by: + +.. math:: + \begin{align} + \boldsymbol{C^T \! M_{\mu f} \, b } - \boldsymbol{M_{\sigma e} \, e} = \mathbf{j_s} \\ + \mathbf{C \, e} = -i \omega \mathbf{b} + \end{align} + +which can be combined to form a single linear system: + +.. math:: + \boldsymbol{[C^T \! M_{\mu f} C } + i\omega \boldsymbol{M_{\sigma e}]} \mathbf{e} = -i \omega \mathbf{j_s} + +where :math:`\boldsymbol{C^T \! M_{\mu f} C } + i\omega \boldsymbol{M_{\sigma e}}` is a sparse matrix and + + - :math:`\boldsymbol{e}` is the discrete approximation to the electric field on edges + - :math:`\boldsymbol{b}` is the discrete approximation to the magnetic flux density on faces + - :math:`\boldsymbol{C}` is the :ref:`discrete curl operator ` + - :math:`\boldsymbol{M_{\sigma e}}` is the :ref:`mass matrix for electrical conductivity ` + - :math:`\boldsymbol{M_{\mu f}}` is the :ref:`mass matrix for the inverse of the magnetic permeability ` + - :math:`\boldsymbol{j_s}` is the discrete representation of the source current density on the edges + diff --git a/docs/content/inner_products.rst b/docs/content/inner_products.rst deleted file mode 100644 index 6c3690d65..000000000 --- a/docs/content/inner_products.rst +++ /dev/null @@ -1,293 +0,0 @@ -Inner Products -************** - -By using the weak formulation of many of the PDEs in geophysical applications, -we can rapidly develop discretizations. Much of this work, however, needs a -good understanding of how to approximate inner products on our discretized -meshes. We will define the inner product as: - -.. math:: - - \left(a,b\right) = \int_\Omega{a \cdot b}{\partial v} - -where a and b are either scalars or vectors. - -.. note:: - - The InnerProducts class is a base class providing inner product matrices - for meshes and cannot run on its own. - - -Example problem for DC resistivity ----------------------------------- - -We will start with the formulation of the Direct Current (DC) resistivity -problem in geophysics. - - -.. math:: - - \frac{1}{\sigma}\vec{j} = \nabla \phi \\ - - \nabla\cdot \vec{j} = q - -In the following discretization, :math:`\sigma` and :math:`\phi` -will be discretized on the cell-centers and the flux, :math:`\vec{j}`, -will be on the faces. We will use the weak formulation to discretize -the DC resistivity equation. - -We can define in weak form by integrating with a general face function -:math:`\vec{f}`: - -.. math:: - - \int_{\Omega}{\sigma^{-1}\vec{j} \cdot \vec{f}} = \int_{\Omega}{\nabla \phi \cdot \vec{f}} - -Here we can integrate the right side by parts, - -.. math:: - - \nabla\cdot(\phi\vec{f})=\nabla\phi\cdot\vec{f} + \phi\nabla\cdot\vec{f} - -and rearrange it, and apply the Divergence theorem. - -.. math:: - - \int_{\Omega}{\sigma^{-1}\vec{j} \cdot \vec{f}} = - - \int_{\Omega}{(\phi \nabla \cdot \vec{f})} - + \int_{\partial \Omega}{ \phi \vec{f} \cdot \mathbf{n}} - -We can then discretize for every cell: - -.. math:: - - v_{\text{cell}} \sigma^{-1} (\mathbf{J}_x \mathbf{F}_x +\mathbf{J}_y \mathbf{F}_y + \mathbf{J}_z \mathbf{F}_z ) = -\phi^{\top} v_{\text{cell}} \mathbf{D}_{\text{cell}} \mathbf{F} + \text{BC} - -.. note:: - - We have discretized the dot product above, but remember that we do not - really have a single vector :math:`\mathbf{J}`, but approximations of - :math:`\vec{j}` on each face of our cell. In 2D that means 2 - approximations of :math:`\mathbf{J}_x` and 2 approximations of - :math:`\mathbf{J}_y`. In 3D we also have 2 approximations of - :math:`\mathbf{J}_z`. - -Regardless of how we choose to approximate this dot product, we can represent -this in vector form (again this is for every cell), and will generalize for -the case of anisotropic (tensor) sigma. - -.. math:: - - \mathbf{F}_c^{\top} (\sqrt{v_{\text{cell}}} \Sigma^{-1} \sqrt{v_{\text{cell}}}) \mathbf{J}_c = - -\phi^{\top} v_{\text{cell}} \mathbf{D}_{\text{cell}} \mathbf{F}) - + \text{BC} - -We multiply by square-root of volume on each side of the tensor conductivity -to keep symmetry in the system. Here :math:`\mathbf{J}_c` is the Cartesian -:math:`\mathbf{J}` (on the faces that we choose to use in our approximation) -and must be calculated differently depending on the mesh: - -.. math:: - \mathbf{J}_c = \mathbf{Q}_{(i)}\mathbf{J}_\text{TENSOR} \\ - \mathbf{J}_c = \mathbf{N}_{(i)}^{-1}\mathbf{Q}_{(i)}\mathbf{J}_\text{Curv} - -Here the :math:`i` index refers to where we choose to approximate this integral, as discussed in the note above. -We will approximate this integral by taking the fluxes clustered around every node of the cell, there are 8 combinations in 3D, and 4 in 2D. We will use a projection matrix :math:`\mathbf{Q}_{(i)}` to pick the appropriate fluxes. So, now that we have 8 approximations of this integral, we will just take the average. For the TensorMesh, this looks like: - -.. math:: - - \mathbf{F}^{\top} - {1\over 8} - \left(\sum_{i=1}^8 - \mathbf{Q}_{(i)}^{\top} \sqrt{v_{\text{cell}}} \Sigma^{-1} \sqrt{v_{\text{cell}}} \mathbf{Q}_{(i)} - \right) - \mathbf{J} - = - -\mathbf{F}^{\top} \mathbf{D}_{\text{cell}}^{\top} v_{\text{cell}} \phi + \text{BC} - -Or, when generalizing to the entire mesh and dropping our general face function: - -.. math:: - - \mathbf{M}^f_{\Sigma^{-1}} \mathbf{J} - = - - \mathbf{D}^{\top} \text{diag}(\mathbf{v}) \phi + \text{BC} - -By defining the faceInnerProduct (8 combinations of fluxes in 3D, 4 in 2D, 2 in 1D) to be: - -.. math:: - - \mathbf{M}^f_{\Sigma^{-1}} = - \sum_{i=1}^{2^d} - \mathbf{P}_{(i)}^{\top} \Sigma^{-1} \mathbf{P}_{(i)} - -Where :math:`d` is the dimension of the mesh. -The :math:`\mathbf{M}^f` is returned when given the input of :math:`\Sigma^{-1}`. - -Here each :math:`\mathbf{P} ~ \in ~ \mathbb{R}^{(d*nC, nF)}` is a combination -of the projection, volume, and any normalization to Cartesian coordinates -(where the dot product is well defined): - -.. math:: - - \mathbf{P}_{(i)} = \sqrt{ \frac{1}{2^d} \mathbf{I}^d \otimes \text{diag}(\mathbf{v})} \overbrace{\mathbf{N}_{(i)}^{-1}}^{\text{Curv only}} \mathbf{Q}_{(i)} - -.. note:: - - This is actually completed for each cell in the mesh at the same time, and the full matrices are returned. - -If ``returnP=True`` is requested in any of these methods the projection matrices are returned as a list ordered by nodes around which the fluxes were approximated:: - - # In 3D - P = [P000, P100, P010, P110, P001, P101, P011, P111] - # In 2D - P = [P00, P10, P01, P11] - # In 1D - P = [P0, P1] - -The derivation for ``edgeInnerProducts`` is exactly the same, however, when we -approximate the integral using the fields around each node, the projection -matrices look a bit different because we have 12 edges in 3D instead of just 6 -faces. The interface to the code is exactly the same. - - -Defining Tensor Properties --------------------------- - -**For 3D:** - -Depending on the number of columns (either 1, 3, or 6) of mu, the material -property is interpreted as follows: - -.. math:: - - \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 & 0 \\ 0 & \mu_{1} & 0 \\ 0 & 0 & \mu_{1} \end{matrix}\right] - - \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 & 0 \\ 0 & \mu_{2} & 0 \\ 0 & 0 & \mu_{3} \end{matrix}\right] - - \vec{\mu} = \left[\begin{matrix} \mu_{1} & \mu_{4} & \mu_{5} \\ \mu_{4} & \mu_{2} & \mu_{6} \\ \mu_{5} & \mu_{6} & \mu_{3} \end{matrix}\right] - -**For 2D:** - - Depending on the number of columns (either 1, 2, or 3) of mu, the material property is interpreted as follows: - -.. math:: - \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 \\ 0 & \mu_{1} \end{matrix}\right] - - \vec{\mu} = \left[\begin{matrix} \mu_{1} & 0 \\ 0 & \mu_{2} \end{matrix}\right] - - \vec{\mu} = \left[\begin{matrix} \mu_{1} & \mu_{3} \\ \mu_{3} & \mu_{2} \end{matrix}\right] - - -Structure of Matrices ---------------------- - -Both the isotropic, and anisotropic material properties result in a diagonal mass matrix. -Which is nice and easy to invert if necessary, however, in the fully anisotropic case which is not aligned with the grid, the matrix is not diagonal. This can be seen for a 3D mesh in the figure below. - -.. plot:: - - import discretize - import numpy as np - import matplotlib.pyplot as plt - mesh = discretize.TensorMesh([10,50,3]) - m1 = np.random.rand(mesh.nC) - m2 = np.random.rand(mesh.nC,3) - m3 = np.random.rand(mesh.nC,6) - M = list(range(3)) - M[0] = mesh.getFaceInnerProduct(m1) - M[1] = mesh.getFaceInnerProduct(m2) - M[2] = mesh.getFaceInnerProduct(m3) - plt.figure(figsize=(13,5)) - for i, lab in enumerate(['Isotropic','Anisotropic','Tensor']): - plt.subplot(131 + i) - plt.spy(M[i],ms=0.5,color='k') - plt.tick_params(axis='both',which='both',labeltop='off',labelleft='off') - plt.title(lab + ' Material Property') - plt.show() - - -Taking Derivatives ------------------- - -We will take the derivative of the fully anisotropic tensor for a 3D mesh, the -other cases are easier and will not be discussed here. Let us start with one -part of the sum which makes up :math:`\mathbf{M}^f_\Sigma` and take the -derivative when this is multiplied by some vector :math:`\mathbf{v}`: - -.. math:: - - \mathbf{P}^\top \boldsymbol{\Sigma} \mathbf{Pv} - -Here we will let :math:`\mathbf{Pv} = \mathbf{y}` and :math:`\mathbf{y}` will have the form: - -.. math:: - - \mathbf{y} = \mathbf{Pv} = - \left[ - \begin{matrix} - \mathbf{y}_1\\ - \mathbf{y}_2\\ - \mathbf{y}_3\\ - \end{matrix} - \right] - -.. math:: - - \mathbf{P}^\top\Sigma\mathbf{y} = - \mathbf{P}^\top - \left[\begin{matrix} - \boldsymbol{\sigma}_{1} & \boldsymbol{\sigma}_{4} & \boldsymbol{\sigma}_{5} \\ - \boldsymbol{\sigma}_{4} & \boldsymbol{\sigma}_{2} & \boldsymbol{\sigma}_{6} \\ - \boldsymbol{\sigma}_{5} & \boldsymbol{\sigma}_{6} & \boldsymbol{\sigma}_{3} - \end{matrix}\right] - \left[ - \begin{matrix} - \mathbf{y}_1\\ - \mathbf{y}_2\\ - \mathbf{y}_3\\ - \end{matrix} - \right] - = - \mathbf{P}^\top - \left[ - \begin{matrix} - \boldsymbol{\sigma}_{1}\circ \mathbf{y}_1 + \boldsymbol{\sigma}_{4}\circ \mathbf{y}_2 + \boldsymbol{\sigma}_{5}\circ \mathbf{y}_3\\ - \boldsymbol{\sigma}_{4}\circ \mathbf{y}_1 + \boldsymbol{\sigma}_{2}\circ \mathbf{y}_2 + \boldsymbol{\sigma}_{6}\circ \mathbf{y}_3\\ - \boldsymbol{\sigma}_{5}\circ \mathbf{y}_1 + \boldsymbol{\sigma}_{6}\circ \mathbf{y}_2 + \boldsymbol{\sigma}_{3}\circ \mathbf{y}_3\\ - \end{matrix} - \right] - -Now it is easy to take the derivative with respect to any one of the -parameters, for example, -:math:`\frac{\partial}{\partial\boldsymbol{\sigma}_1}` - -.. math:: - \frac{\partial}{\partial \boldsymbol{\sigma}_1}\left(\mathbf{P}^\top\Sigma\mathbf{y}\right) - = - \mathbf{P}^\top - \left[ - \begin{matrix} - \text{diag}(\mathbf{y}_1)\\ - 0\\ - 0\\ - \end{matrix} - \right] - -Whereas :math:`\frac{\partial}{\partial\boldsymbol{\sigma}_4}`, for -example, is: - -.. math:: - \frac{\partial}{\partial \boldsymbol{\sigma}_4}\left(\mathbf{P}^\top\Sigma\mathbf{y}\right) - = - \mathbf{P}^\top - \left[ - \begin{matrix} - \text{diag}(\mathbf{y}_2)\\ - \text{diag}(\mathbf{y}_1)\\ - 0\\ - \end{matrix} - \right] - -These are computed for each of the 8 projections, horizontally concatenated, -and returned. diff --git a/docs/content/references.bib b/docs/content/references.bib new file mode 100644 index 000000000..cf3718f09 --- /dev/null +++ b/docs/content/references.bib @@ -0,0 +1,26 @@ + + +@Article{HymanShashkov1999, + Title = {Mimetic Discretizations for Maxwell’s Equations}, + Author = {Hyman, James M. and Shashkov, Mikhail}, + Journal = {Journal of Computational Geophysics}, + Year = {1999}, + Pages = {881--909}, + Volume = {151}, + Url = {https://cnls.lanl.gov/~shashkov/papers/maxjcp.pdf} +} + +@Book{griffiths1999, + Title = {Introduction to electrodynamics}, + Author = {Griffiths, David J}, + Publisher = {Prentice-Hall}, + Year = {1999} +} + +@Book{haber2014, + Title = {Computational Methods in Geophysical Electromagnetics}, + Author = {Haber, Eldad}, + Publisher = {SIAM}, + Year = {2014}, + Volume = {1} +} \ No newline at end of file diff --git a/docs/content/references.rst b/docs/content/references.rst new file mode 100644 index 000000000..d21d93a49 --- /dev/null +++ b/docs/content/references.rst @@ -0,0 +1,5 @@ +References +========== + +.. bibliography:: references.bib + :all: \ No newline at end of file diff --git a/docs/content/theory/derivation_examples_advection_diffusion.rst b/docs/content/theory/derivation_examples_advection_diffusion.rst new file mode 100644 index 000000000..f1c140e2d --- /dev/null +++ b/docs/content/theory/derivation_examples_advection_diffusion.rst @@ -0,0 +1,220 @@ +.. _derivation_examples_advection_diffusion: + +Advection and Diffusion with Zero Neumann Boundary Condition +************************************************************ + +Here we provide the derivation for solving the advection-diffusion equation using the finite volume method. +We assume the fluid is incompressible. Key lessons include: + + - Implementing boundary conditions + - Solving time-dependent PDEs + - Strategies for applying finite volume to 2nd order PDEs + +**Tutorial:** :ref:`Advection-Diffusion Equation ` + +Setup +----- + +If we assume the fluid is incompressible (i.e. :math:`\nabla \cdot \vec{u} = 0`), +the advection-diffusion equation with zero Neumann boundary conditions is given by: + +.. math:: + \begin{align} + & p_t = \nabla \cdot \alpha \nabla p - \vec{u} \cdot \nabla p + s \\ + & \textrm{s.t.} \;\;\; \frac{\partial p}{\partial n} \Bigg |_{\partial \Omega} = 0 \\ + & \textrm{and} \;\;\; p(t=0) = 0 + \end{align} + :label: derivation_examples_advection_diffusion_1 + +where + + - :math:`p` is the unknown variable + - :math:`p_t` is its time-derivative + - :math:`\alpha` defines the diffusivity within the domain + - :math:`\vec{u}` is the velocity field + - :math:`s` is the source term + +We will consider the case where there is a single point source within our domain. +Where :math:`s_0` is a constant: + +.. math:: + s = s_0 \delta ( \vec{r} ) + +Direct implementation of the finite volume method is more challenging on higher order PDEs. +Therefore, we redefine the problem as a set of first order PDEs as follows: + +.. math:: + \begin{align} + p_t = \nabla \cdot \vec{j} - \vec{u} \cdot \vec{w} + s \;\;\;\; &(1)\\ + \vec{w} = \nabla p \;\;\;\; &(2)\\ + \alpha^{-1} \vec{j} = \vec{w} \;\;\;\; &(3) + \end{align} + :label: derivation_examples_advection_diffusion_2 + +We then take the inner products between the expressions in equation :eq:`derivation_examples_advection_diffusion_2` and an appropriate test function. + +Discretization in Space +----------------------- + +Because this is a time-dependent problem, we must consider discretization in both space and time. +We generally begin by discretizing in space. Where :math:`\boldsymbol{p}` is the discrete representation of :math:`p`, +we will discretize such that :math:`\boldsymbol{p}` lives on the cell centers. If :math:`\boldsymbol{p}` lives +at cell centers, it makes sense for the discrete representations :math:`\vec{w}`, :math:`\vec{j}` and :math:`\vec{u}` +in expression :eq:`derivation_examples_advection_diffusion_2` to live on the faces. + +First Equation +^^^^^^^^^^^^^^ + +Let :math:`\psi` be a scalar test function whose discrete representation :math:`\boldsymbol{\psi}` lives at cell centers. +The inner product between :math:`\psi` and the first equation +in :eq:`derivation_examples_advection_diffusion_2` is given by: + +.. math:: + \int_\Omega \psi p_t \, dv = \int_\Omega \psi \nabla \cdot \vec{j} \, dv + \int_\Omega \psi (\vec{u} \cdot \vec{w}) \, dv + \int_\Omega \psi s \, dv + :label: derivation_examples_advection_diffusion_3 + +According to :ref:`basic inner products `, the term to the left of the equals sign is approximated by: + +.. math:: + \int_\Omega \psi p_t \, dv \approx \boldsymbol{\psi^T M_c \, p_t} + :label: derivation_examples_advection_diffusion_4a + +where :math:`\boldsymbol{M_c}` is the :ref:`inner product matrix for quantities at cell centers `. + +Since :math:`\boldsymbol{\psi}` lives at cell centers, then so must the divergence of :math:`\vec{j}`. +This implies the discrete vector :math:`\boldsymbol{j}` must live on the faces. +And according to :ref:`inner products with differential operators `: + +.. math:: + \int_\Omega \psi \nabla \cdot \vec{j} \, dv \approx \boldsymbol{\psi^T M_c D \, j} + :label: derivation_examples_advection_diffusion_4b + +where :math:`\boldsymbol{D}` is the :ref:`discrete divergence matrix `. + +For the next term in equation :eq:`derivation_examples_advection_diffusion_3`, we must take the +dot product of vectors :math:`\vec{u}` and :math:`\vec{w}`. Here, we let the corresponding +discrete representations :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` live on the faces; +this is because we need the gradient of :math:`\boldsymbol{p}` to live on the faces. In doing so, we need a set of operations which +multiplies the components of the dot product and sums them at the cell center. +Where :math:`\boldsymbol{A_{fc}}` is the :ref:`scalar averaging matrix from faces to cell centers ` +and :math:`c` = 1, 2 or 3 is the dimension of the problem, the inner product is approximated by: + +.. math:: + \int_\Omega \psi (\vec{u} \cdot \vec{w}) \, dv \approx c \, \boldsymbol{\psi^T M_c A_{fc}} diag(\boldsymbol{u}) \, \boldsymbol{w} + :label: derivation_examples_advection_diffusion_4c + +For the final term in :eq:`derivation_examples_advection_diffusion_3`, our inner product contains the delta function. +As a result: + +.. math:: + \int_\Omega \psi q \, dv \approx \boldsymbol{\psi^T q} + :label: derivation_examples_advection_diffusion_4d + +where :math:`\boldsymbol{q}` is a discrete representation for the integrated source term in each cell. +In this case, :math:`\boldsymbol{q_i}=s_0` at the center of the cell containing the source +It is zero for every other cell. + +If we substitute the inner product approximations from expressions +:eq:`derivation_examples_advection_diffusion_4a`, :eq:`derivation_examples_advection_diffusion_4b`, +:eq:`derivation_examples_advection_diffusion_4c` and :eq:`derivation_examples_advection_diffusion_4d` +into equation :eq:`derivation_examples_advection_diffusion_3`, we obtain: + +.. math:: + \boldsymbol{\psi^T M_c \, p_t} = \boldsymbol{\psi^T M_c D \, j} - + c\, \boldsymbol{\psi^T M_c A_{fc}} \, diag(\boldsymbol{u}) \, \boldsymbol{w} + \boldsymbol{\psi^T q} + :label: derivation_examples_advection_diffusion_5 + +Second Equation +^^^^^^^^^^^^^^^ + +In the second equation of :eq:`derivation_examples_advection_diffusion_2`, we use what we learned in +:ref:`inner products with differential operators `. +Let :math:`\vec{f}` be a vector test function whose discrete representation :math:`\boldsymbol{f}` lives on the faces. +After using the vector identity :math:`\vec{f} \cdot \nabla p = \nabla \cdot p\vec{f} - p \nabla \cdot \vec{f}` +and applying the divergence theorem: + +.. math:: + \int_\Omega \vec{f} \cdot \vec{w} = - \int_\Omega p \nabla \cdot \vec{f} \, dv + \oint_{\partial \Omega} p \hat{n} \cdot \vec{f} \, da + :label: derivation_examples_advection_diffusion_6 + +The discrete approximation is given by: + +.. math:: + \boldsymbol{f^T M_f \, w} = - \boldsymbol{f^T D^T M_c \, p + f^T B \, p} + :label: derivation_examples_advection_diffusion_7 + +where :math:`\boldsymbol{B}` is a sparse matrix that imposes boundary conditions correctly on :math:`p`. + +Third Equation +^^^^^^^^^^^^^^ + +In the third equation of :eq:`derivation_examples_advection_diffusion_2`, we use what we learned in +:ref:`inner products with contitutive relationships `; assume the diffusibility :math:`\alpha` is linear isotropic. +The inner product with a vector test function :math:`\vec{f}` whose discrete representation :math:`\boldsymbol{f}` lives on the faces +is given by: + +.. math:: + \int_\Omega \vec{f} \cdot \alpha^{\! -1} \vec{j} \, dv = \int_\Omega \vec{f} \cdot \vec{w} \, dv + :label: derivation_examples_advection_diffusion_8 + +Our formulation defines the diffusivity in terms of its inverse. As a result, the approximation of the inner +products is given by: + +.. math:: + \boldsymbol{f^T M_\alpha \, j} = \boldsymbol{f^T M_f w} + :label: derivation_examples_advection_diffusion_9 + +where :math:`\boldsymbol{M_\alpha}` is the :ref:`inner product matrix at faces for the reciprocal of the diffusivity `. + +Combining the Expressions +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here, we substitute the discrete representations of the inner products from expressions +:eq:`derivation_examples_advection_diffusion_7` and :eq:`derivation_examples_advection_diffusion_9` +into :eq:`derivation_examples_advection_diffusion_5` and factor like-terms. + +Let :math:`\boldsymbol{\tilde{G}}` represent a modified gradient operator with the boundary conditions implemented: + +.. math:: + \boldsymbol{\tilde{G}} = \boldsymbol{-D^T M_c + B} + :label: derivation_examples_advection_diffusion_10 + +The system of equations discretized in space is given by: + +.. math:: + \boldsymbol{p_t} = \boldsymbol{\big [ D \, M_\alpha^{-1} \tilde{G}} - + c\, \boldsymbol{A_{fc}} diag(\boldsymbol{u}) \, \boldsymbol{M_f^{-1} \tilde{G} \big ] \, p} + \boldsymbol{M_c^{-1} \, q} + :label: derivation_examples_advection_diffusion_11 + +Discretization in Time +---------------------- + +To discretize in time, let us re-express equations :eq:`derivation_examples_advection_diffusion_11` as: + +.. math:: + \boldsymbol{p_t} = \boldsymbol{- M \, p + s} + :label: derivation_examples_advection_diffusion_12 + +where + +.. math:: + \boldsymbol{M} = - \boldsymbol{D \, M_\alpha^{-1} \tilde{G}} + + c\, \boldsymbol{A_{fc}} diag(\boldsymbol{u}) \, \boldsymbol{M_f^{-1} \tilde{G}} + :label: derivation_examples_advection_diffusion_13 + +and + +.. math:: + \boldsymbol{s} = \boldsymbol{M_c^{-1} \, q} + :label: derivation_examples_advection_diffusion_14 + +There are a multitude of ways in which discretization in time can be implemented. +A stable and easy method to implement is the backward Euler. +By implementing the backward Euler, we must solve the following linear system +at each time step :math:`k`: + +.. math:: + \big [ \boldsymbol{I} + \Delta t \, \boldsymbol{M} \big ] \, \boldsymbol{p}^{k+1} = \boldsymbol{p}^k + \Delta t \, \boldsymbol{s} + :label: derivation_examples_advection_diffusion_14 + +where :math:`\boldsymbol{I}` is the identity matrix and :math:`\Delta t` is the step length. \ No newline at end of file diff --git a/docs/content/theory/derivation_examples_index.rst b/docs/content/theory/derivation_examples_index.rst new file mode 100644 index 000000000..c743b3de3 --- /dev/null +++ b/docs/content/theory/derivation_examples_index.rst @@ -0,0 +1,20 @@ +.. _derivation_examples_index: + +Solving PDEs Examples +********************* + +Here, we provide examples of full derivations for discretizing PDEs with the finite volume method (:cite:`haber2014,HymanShashkov1999`). + + +**Contents:** + +.. toctree:: + :maxdepth: 1 + + derivation_examples_poisson + derivation_examples_advection_diffusion + +**Tutorials:** + +- :ref:`Poisson Equation with Zero Neumann Boundary Condition ` +- :ref:`Advection-Diffusion Equation with Zero Neumann Boundary Condition ` \ No newline at end of file diff --git a/docs/content/theory/derivation_examples_poisson.rst b/docs/content/theory/derivation_examples_poisson.rst new file mode 100644 index 000000000..7cc869115 --- /dev/null +++ b/docs/content/theory/derivation_examples_poisson.rst @@ -0,0 +1,205 @@ +.. _derivation_examples_poisson: + +Poisson Equation with Zero Neumann Boundary Condition +***************************************************** + +Here we provide the derivation for solving the Poisson equation with zero Neumann boundary conditions using the +finite volume method. +Derivations are provided for discretization of the solution on both the nodes and at cell centers. +Key lessons include: + + - Differences between solving the problem on the nodes and the cell centers + - Basic discretization of point sources + - Choosing the right approach for natural boundary conditions + - Implementing the zero Neumann condition when the discrete boundary condition term is not zero + +For our example, we consider Gauss's law of electrostatics. +Our goal is to compute the electric potential (:math:`\phi`) and electric fields (:math:`\boldsymbol{e}`) that result from +a positive and a negative point charge separated by some distance. + +**Tutorial:** :ref:`Poisson Equation with Zero Neumann Boundary Condition ` + +Setup +----- + +Starting with Gauss's law and Faraday's law in the case of electrostatics (:cite:`griffiths1999`): + +.. math:: + &\nabla \cdot \vec{e} = \frac{\rho}{\epsilon_0} \\ + &\nabla \times \vec{e} = \boldsymbol{0} \;\;\; \Rightarrow \;\;\; \vec{e} = -\nabla \phi \\ + &\textrm{s.t.} \;\;\; \hat{n} \cdot \vec{e} \, \Big |_{\partial \Omega} = -\frac{\partial \phi}{\partial n} \, \Big |_{\partial \Omega} = 0 + :label: derivation_examples_electrostatics_1 + +where :math:`\rho` is the charge density and :math:`\epsilon_0` is the permittivity of free space. +The Neumann boundary condition on :math:`\phi` implies no electric flux leaves the system. +For 2 point charges of equal and opposite sign, the charge density is given by: + +.. math:: + \rho = \rho_0 \big [ \delta ( \boldsymbol{r_+}) - \delta (\boldsymbol{r_-} ) \big ] + :label: derivation_examples_electrostatics_2 + +To solve this problem numerically, we first +take the inner product of each equation with an appropriate test function. +Where :math:`\psi` is a scalar test function and :math:`\vec{u}` is a +vector test function: + +.. math:: + \int_\Omega \psi (\nabla \cdot \vec{e}) \, dv = \frac{1}{\epsilon_0} \int_\Omega \psi \rho \, dv + :label: derivation_examples_electrostatics_3 + +and + +.. math:: + \int_\Omega \vec{u} \cdot \vec{e} \, dv = - \int_\Omega \vec{u} \cdot (\nabla \phi ) \, dv + :label: derivation_examples_electrostatics_4 + + +In the case of Gauss' law, we have a volume integral containing the Dirac delta function. +Thus expression :eq:`derivation_examples_electrostatics_3` becomes: + +.. math:: + \int_\Omega \psi (\nabla \cdot \vec{e}) \, dv = \frac{1}{\epsilon_0} \psi \, q + :label: derivation_examples_electrostatics_5 + +where :math:`q` represents an integrated charge density. +To apply the finite volume method, we must choose whether to discretize the scalar quantity :math:`\phi` at the nodes or cell centers. + +Electic Potential on the Nodes +------------------------------ + +Here, we let :math:`\boldsymbol{\phi}` be the discrete representation of the electic potential :math:`\phi` on the nodes +and :math:`\boldsymbol{e}` be the discrete representation of the electic field :math:`\vec{e}` on the edges. + +**First Expression:** + +To implement the finite volume approach, we begin by approximating the inner products in expression :eq:`derivation_examples_electrostatics_4`. +The left-hand side can be approximated according to :ref:`basic inner products ` . +And in :ref:`inner products with differential operators `, we learned how to approximate the right-hand side. +The discrete representation of expression :eq:`derivation_examples_electrostatics_4` is therefore given by: + +.. math:: + \boldsymbol{u^T M_e \, e} = - \boldsymbol{u^T M_e G \, \phi} + :label: derivation_examples_electrostatics_6 + +where + + - :math:`\boldsymbol{M_e}` is the :ref:`inner product matrix at edges ` + - :math:`\boldsymbol{G}` is the :ref:`discrete gradient operator ` + +**Second Expression:** + +Now we approximate the inner products in expression :eq:`derivation_examples_electrostatics_5`. +For the left-hand side, we must use the identity :math:`\psi \nabla \cdot \vec{e} = \nabla \cdot \psi\vec{e} - \vec{e} \cdot \nabla \psi` +and apply the divergence theorem such that expression :eq:`derivation_examples_electrostatics_5` becomes: + +.. math:: + - \int_\Omega \vec{e} \cdot \nabla \psi \, dv + \oint_{\partial \Omega} \psi (\hat{n} \cdot \vec{e}) \, da = \frac{1}{\epsilon_0} \psi \, q + :label: derivation_examples_electrostatics_7 + +Since :math:`\hat{n} \cdot \vec{e}` is zero on the boundary, the surface integral is equal to zero. +The left-hand side can be approximated according to :ref:`inner products with differential operators `. +:math:`\boldsymbol{\psi}` and :math:`\boldsymbol{q}` are defined such that their discrete representations :math:`\psi` and :math:`\rho` +must live on the nodes. The discrete approximation to expression :eq:`derivation_examples_electrostatics_7` is given by: + +.. math:: + - \boldsymbol{\psi^T G^T M_e \, e} = \frac{1}{\epsilon_0} \boldsymbol{\psi^T q} + :label: derivation_examples_electrostatics_8 + +where :math:`\boldsymbol{q}` is a discrete representation of the integrated charge density. + +The easiest way to discretize the source is to let :math:`\boldsymbol{q_i}=\rho_0` at the nearest node to the positive charge and +let :math:`\boldsymbol{q_i}=-\rho_0` at the nearest node to the negative charge. +The value is zero for all other nodes. + +**Discretized System:** + +By combining the discrete representations from expressions :eq:`derivation_examples_electrostatics_6` and :eq:`derivation_examples_electrostatics_8` +we obtain: + +.. math:: + \boldsymbol{G^T M_e G \, \phi} = \frac{1}{\epsilon_0} \boldsymbol{q} + :label: derivation_examples_electrostatics_9 + +Let :math:`\boldsymbol{A} = \boldsymbol{G^T M_e G}`. +The linear system has a single null vector. +To remedy this, we set a reference potential on the boundary +by setting :math:`A_{0,0} = 1` and by setting all other values in the row to 0. +Once the electric potential at nodes has been computed, the electric field on the edges can be computed using expression :eq:`derivation_examples_electrostatics_6`: + +.. math:: + \boldsymbol{e} = - \boldsymbol{G \, \phi} + + +Electic Potential at Cell Centers +--------------------------------- + +Here, we let :math:`\boldsymbol{\phi}` be the discrete representation of the electic potential :math:`\phi` at cell centers +and :math:`\boldsymbol{e}` be the discrete representation of the electic field :math:`\vec{e}` on the faces. +It is acceptable to discretize the electric field on the faces in this case because the dielectric permittivity of the domain +is constant and the electric field at the faces is continuous. + +**First Expression:** + +To implement the finite volume approach, we begin by approximating the inner products in expression :eq:`derivation_examples_electrostatics_5`. +The left-hand side can be approximated according to :ref:`inner products with differential operators `. +Where :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{q}` are discrete representations of :math:`\psi` and :math:`\rho` living at cell centers: + +.. math:: + \boldsymbol{\psi^T M_c D e} = \frac{1}{\epsilon_0} \boldsymbol{\psi^T q} + :label: derivation_examples_electrostatics_10 + +where + + - :math:`\boldsymbol{M_c}` is the :ref:`inner product matrix at cell centers ` + - :math:`\boldsymbol{D}` is the :ref:`discrete divergence operator ` + - :math:`\boldsymbol{q}` is a discrete representation for the integrated charge density for each cell. + +In this case, :math:`\boldsymbol{q_i}=\rho_0` at the center of the cell containing the positive charge and +:math:`\boldsymbol{q_i}=-\rho_0` at the center of the cell containing the negative charge. +It is zero for every other cell. + + + +**Second Expression:** + +We now approximate the inner products in expression :eq:`derivation_examples_electrostatics_4`. +The left-hand side can be approximated according to :ref:`basic inner products ` . +And in :ref:`inner products with differential operators `, we learned how to approximate the right-hand side. +For the right-hand side, we must use the identity :math:`\vec{u} \cdot \nabla \phi = \nabla \cdot \phi\vec{u} - \phi \nabla \cdot \vec{u}` +and apply the divergence theorem such that expression :eq:`derivation_examples_electrostatics_4` becomes: + +.. math:: + \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \phi \nabla \cdot \vec{u} \, dv - \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da + :label: derivation_examples_electrostatics_11 + +According to expression :eq:`derivation_examples_electrostatics_1`, :math:`\hat{n} \cdot \vec{e}`, +:math:`\frac{\partial \phi}{\partial n} = 0 on the boundaries. +To accurately compute the electric potentials at cell centers, we must implement the boundary conditions such that: + +.. math:: + \boldsymbol{u^T M_f \, e} = \boldsymbol{u^T D^T M_c \, \phi} - \boldsymbol{u^T B \, \phi} = - \boldsymbol{\tilde{G} \, \phi} + :label: derivation_examples_electrostatics_12 + +where + + - :math:`\boldsymbol{M_c}` is the :ref:`inner product matrix at cell centers ` + - :math:`\boldsymbol{M_f}` is the :ref:`inner product matrix at faces ` + - :math:`\boldsymbol{D}` is the :ref:`discrete divergence operator ` + - :math:`\boldsymbol{B}` is a sparse matrix that imposes the Neumann boundary condition + - :math:`\boldsymbol{\tilde{G}} = - \boldsymbol{D^T M_c} + \boldsymbol{B}` acts as a modified gradient operator with boundary conditions included + +**Discretized System:** + +By combining the discrete representations from expressions :eq:`derivation_examples_electrostatics_10` and :eq:`derivation_examples_electrostatics_12` +we obtain: + +.. math:: + - \boldsymbol{M_c D M_f^{-1} \tilde{G} \, \phi} = \frac{1}{\epsilon_0} \boldsymbol{q} + :label: derivation_examples_electrostatics_13 + +Once the electric potential at cell centers has been computed, the electric field on the faces can be computed using expression :eq:`derivation_examples_electrostatics_12`: + +.. math:: + \boldsymbol{e} = - \boldsymbol{M_f^{-1} \tilde{G} \, \phi} + + diff --git a/docs/content/theory/inner_products_anisotropic.rst b/docs/content/theory/inner_products_anisotropic.rst new file mode 100644 index 000000000..a1233114c --- /dev/null +++ b/docs/content/theory/inner_products_anisotropic.rst @@ -0,0 +1,573 @@ +.. _inner_products_anisotropic: + +Anisotropic Constitutive Relationships +************************************** + +Summary +------- + +A constitutive relationship quantifies the response of a material to an external stimulus. +Examples include Ohm's law and Hooke's law. For practical applications of the finite volume method, +we may need to take the inner product of expressions containing constitutive relationships. + +Let :math:`\vec{v}` and :math:`\vec{w}` be two physically related quantities. +If their relationship is anisotropic (defined by a tensor :math:`\Sigma`), the constitutive relation is of the form: + +.. math:: + \vec{v} = \Sigma \vec{w} + :label: inner_product_anisotropic + +where + +.. math:: + \mathbf{In \; 2D:} \; + \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} \\ + \sigma_{yx} & \sigma_{yy} \end{bmatrix} + \;\;\;\;\;\;\;\; \mathbf{In \; 3D:} \; + \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ + \sigma_{yx} & \sigma_{yy} & \sigma_{yz} \\ + \sigma_{zx} & \sigma_{zy} & \sigma_{zz} \end{bmatrix} + +Note that for real materials, the tensor is symmetric and has 6 independent variables +(i.e. :math:`\sigma_{pq}=\sigma_{qp}` for :math:`p,q=x,y,z`). +Here we show that for anisotropic constitutive relationships, the inner +product between a vector :math:`\vec{u}` and the right-hand side of +equation :eq:`inner_product_anisotropic` is approximated by: + +.. math:: + (\vec{u}, \Sigma \vec{w} ) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{u^T M w} + :label: inner_product_anisotropic_general + +where :math:`\boldsymbol{M}` represents an *inner-product matrix*, and vectors +:math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are discrete variables that live +on the mesh. It is important to note a few things about the +inner-product matrix in this case: + + 1. It depends on the dimensions and discretization of the mesh + 2. It depends on where the discrete variables live; e.g. edges, faces, nodes, centers + 3. It depends on the spacial variation of the tensor property :math:`\Sigma` + +For this class of inner products, the corresponding form of the inner product matricies for +discrete quantities living on various parts of the mesh are shown below. + +**Tutorial:** To construct the inner product matrix and/or approximate inner products of this type, see the :ref:`tutorial on inner products with constitutive relationships ` + +**Diagonal Anisotropic:** + +For the diagonal anisotropic case, the tensor characterzing the material properties +has the form: + +.. math:: + \mathbf{In \; 2D:} \; + \Sigma = \begin{bmatrix} \sigma_{x} & 0 \\ + 0 & \sigma_{y} \end{bmatrix} + \;\;\;\;\;\;\;\; \mathbf{In \; 3D:} \; + \Sigma = \begin{bmatrix} \sigma_{x} & 0 & 0 \\ + 0 & \sigma_{y} & 0 \\ + 0 & 0 & \sigma_{z} \end{bmatrix} + :label: inner_product_tensor_diagonal + + +The inner product matrix defined in expression :eq:`inner_product_anisotropic_general` is given by: + +.. math:: + \textrm{Vectors on faces:} \; \boldsymbol{M_{\Sigma f}} &= \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \sigma \big )} \boldsymbol{P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_{\Sigma e}} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \sigma \big )} \boldsymbol{P_e} + +where :math:`\boldsymbol{\sigma}` organizes vectors :math:`\boldsymbol{\sigma_x}`, +:math:`\boldsymbol{\sigma_y}` and :math:`\boldsymbol{\sigma_z}` as: + +.. math:: + \boldsymbol{\sigma} = \begin{bmatrix} \boldsymbol{\sigma_x} \\ \boldsymbol{\sigma_y} \\ \boldsymbol{\sigma_z} \\ \end{bmatrix} + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from faces and edges to cell centers, respectively + +**Fully Anisotropic:** + +For a fully anisotropic case, the tensor characterizing the material properties +has the form is given by: + +.. math:: + \mathbf{In \; 2D:} \; + \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} \\ + \sigma_{yx} & \sigma_{yy} \end{bmatrix} + \;\;\;\;\;\;\;\; \mathbf{In \; 3D:} \; + \Sigma = \begin{bmatrix} \sigma_{xx} & \sigma_{xy} & \sigma_{xz} \\ + \sigma_{yx} & \sigma_{yy} & \sigma_{yz} \\ + \sigma_{zx} & \sigma_{zy} & \sigma_{zz} \end{bmatrix} + :label: inner_product_tensor + +The inner product matrix defined in expression :eq:`inner_product_anisotropic_general` is given by: + +.. math:: + \textrm{Vectors on faces:} \; \boldsymbol{M_{\Sigma f}} &= \frac{1}{4} \boldsymbol{P_f^T Q_u^T} \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \sigma \big )} \boldsymbol{Q_w P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_{\Sigma e}} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T Q_u^T} \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \sigma \big )} \boldsymbol{Q_w P_e} + +where :math:`\boldsymbol{\sigma}` is a large vector that organizes vectors :math:`\boldsymbol{\sigma_{pq}}` for :math:`p,q=x,y,z` as: + +.. math:: + \boldsymbol{\sigma} = \begin{bmatrix} + \boldsymbol{\sigma_{xx}} , \; \boldsymbol{\sigma_{xy}} , \; \boldsymbol{\sigma_{xz}} , \; + \boldsymbol{\sigma_{yx}} , \; \boldsymbol{\sigma_{yy}} , \; \boldsymbol{\sigma_{yz}} , \; + \boldsymbol{\sigma_{zx}} , \; \boldsymbol{\sigma_{zy}} , \; \boldsymbol{\sigma_{zz}} \end{bmatrix}^T + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from faces and edges to cell centers, respectively + - :math:`\boldsymbol{Q_u}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z} ]^T` + - :math:`\boldsymbol{Q_w}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_z} \; \boldsymbol{u_z} ]^T` + + +Diagonally Anisotropic Case +--------------------------- + +Vectors on Cell Faces +^^^^^^^^^^^^^^^^^^^^^ + +We want to approximate the inner product between a vector quantity :math:`\vec{u}` and the product of +:math:`\Sigma` and :math:`\vec{w}`, where :math:`\Sigma` given in expression :eq:`inner_product_tensor_diagonal`. +Here, we discretize such that :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live on cell faces. Our goal is to construct the inner product matrix :math:`\boldsymbol{M}` such that: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{u^T \, M \, w} + :label: inner_product_anisotropic_faces + +We must respect the dot product and the tensor. For vectors defined on cell faces, we discretize such that the +x-component of the vectors live on the x-faces, the y-component lives y-faces and the z-component +lives on the z-faces. For a single cell, this is illustrated in 2D and 3D below. By decomposing the +domain into a set of finite cells, we assume the tensor properties are spacial invariant within each cell. + +.. figure:: ../../images/face_discretization.png + :align: center + :width: 600 + +As we can see there are 2 faces for each component. Therefore, we need to project each component of the +vector from its faces to the cell centers and take their averages separately. We must also recognize that +x-components are only multiplied by :math:`\sigma_x`, y-components by :math:`\sigma_y` and z-components +by :math:`\sigma_z`. + +For a single cell :math:`i` with volume :math:`v` and tensor properties defined by +:math:`\sigma_x`, :math:`\sigma_y`, :math:`\sigma_z` +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{4} + \sum_{p=x,y} \sigma_{p} \Big ( u_p^{(1)} + u_p^{(2)} \Big ) \Big ( w_p^{(1)} + w_p^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{4} + \sum_{p=x,y,z} \sigma_{p} \Big ( u_p^{(1)} + u_p^{(2)} \Big ) \Big ( w_p^{(1)} + w_p^{(2)} \Big ) + \end{align} + :label: inner_product_anisotropic_faces_1 + +where superscripts :math:`(1)` and :math:`(2)` denote face 1 and face 2, respectively. +Using the contribution for each cell described in expression :eq:`inner_product_anisotropic_faces_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_anisotropic_faces`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_f}` which projects quantities on the x, y and z faces separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell faces as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{\boldsymbol{u} \, M_{\Sigma f}} \, \boldsymbol{w} + +The inner product matrix defined in the previous expression is given by: + +.. math:: + \boldsymbol{M_{\Sigma f}} = \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \sigma \big )} \boldsymbol{P_f} + +where :math:`\boldsymbol{\sigma}` organizes vectors :math:`\boldsymbol{\sigma_x}`, +:math:`\boldsymbol{\sigma_y}` and :math:`\boldsymbol{\sigma_z}` as: + +.. math:: + \boldsymbol{\sigma} = \begin{bmatrix} \boldsymbol{\sigma_x} \\ \boldsymbol{\sigma_y} \\ \boldsymbol{\sigma_z} \\ \end{bmatrix} + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_f}` is a projection matrix that maps quantities from faces to cell centers + +Vectors on Cell Edges +^^^^^^^^^^^^^^^^^^^^^ + +We want to approximate the inner product between a vector quantity :math:`\vec{u}` and the product of +:math:`\Sigma` and :math:`\vec{w}`, where :math:`\Sigma` given in expression :eq:`inner_product_tensor_diagonal`. +Here, we discretize such that :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live on cell edges. Our goal is to construct the inner product matrix :math:`\boldsymbol{M}` such that: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{u^T \, M \, w} + :label: inner_product_anisotropic_edges + +We must respect the dot product and the tensor. For vectors defined on cell edges, we discretize such that the +x-component of the vectors live on the x-edges, the y-component lives y-edges and the z-component +lives on the z-edges. This is illustrated in 2D and 3D below. By decomposing the +domain into a set of finite cells, we assume the tensor properties are spacial invariant within each cell. + +.. figure:: ../../images/edge_discretization.png + :align: center + :width: 600 + +As we can see there are 2 edges for each component in 2D and 4 edges for each component in 3D. +Therefore, we need to project each component of the +vector from its edges to the cell centers and take their averages separately. +We must also recognize that +x-components are only multiplied by :math:`\sigma_x`, y-components by :math:`\sigma_y` and z-components +by :math:`\sigma_z`. + +For a single cell :math:`i` with volume :math:`v` and tensor properties defined by +:math:`\sigma_x`, :math:`\sigma_y`, :math:`\sigma_z` +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{4} + \sum_{p=x,y} \sigma_{p} \Big ( u_p^{(1)} + u_p^{(2)} \Big ) \Big ( w_p^{(1)} + w_p^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{16} + \sum_{p=x,y,z} \sigma_{p} \Big ( u_p^{(1)} + u_p^{(2)} + u_p^{(3)} + u_p^{(4)} \Big ) + \Big ( w_p^{(1)} + w_p^{(2)} + w_p^{(3)} + w_p^{(4)} \Big ) + \end{align} + :label: inner_product_anisotropic_edges_1 + +where the superscripts :math:`(1)` to :math:`(4)` denote a particular edges. +Using the contribution for each cell described in expression :eq:`inner_product_anisotropic_edges_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_anisotropic_edges`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_e}` which projects quantities on the x, y and z edges separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell edges as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{\boldsymbol{u} \, M_{\Sigma e} \, \boldsymbol{w}} + +The inner product matrix defined in the previous expression is given by: + +.. math:: + \boldsymbol{M_{\Sigma e}} = \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \sigma \big )} \boldsymbol{P_e} + +where :math:`\boldsymbol{\sigma}` organizes vectors :math:`\boldsymbol{\sigma_x}`, +:math:`\boldsymbol{\sigma_y}` and :math:`\boldsymbol{\sigma_z}` as: + +.. math:: + \boldsymbol{\sigma} = \begin{bmatrix} \boldsymbol{\sigma_x} \\ \boldsymbol{\sigma_y} \\ \boldsymbol{\sigma_z} \\ \end{bmatrix} +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_e}` is a projection matrix that maps quantities from edges to cell centers + +Fully Anisotropic Case +---------------------- + +Vectors on Cell Faces +^^^^^^^^^^^^^^^^^^^^^ + +We want to approximate the inner product between a vector quantity :math:`\vec{u}` and the product of +:math:`\Sigma` and :math:`\vec{w}`, where :math:`\Sigma` given in expression :eq:`inner_product_tensor`. +Here, we discretize such that :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live on cell faces. Our goal is to construct the inner product matrix :math:`\boldsymbol{M}` such that: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{u^T \, M \, e} + :label: inner_product_anisotropic_faces + +We must respect the dot product and the tensor. For vectors defined on cell faces, we discretize such that the +x-component of the vectors live on the x-faces, the y-component lives y-faces and the z-component +lives on the z-faces. For a single cell, this is illustrated in 2D and 3D below. By decomposing the +domain into a set of finite cells, we assume the tensor properties are spacial invariant within each cell. + +.. figure:: ../../images/face_discretization.png + :align: center + :width: 600 + +As we can see there are 2 faces for each component. Therefore, we need to project each component of the +vector from its faces to the cell centers and take their averages separately. We must also recognize that +different parameters :math:`\sigma_{pq}` for :math:`p,q=x,y,z` multiply different components of the vectors. + +For a single cell :math:`i` with volume :math:`v` and tensor properties defined by +:math:`\sigma_{pq}` for :math:`p,q=x,y,z`, +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{4} + \sum_{p,q=x,y} \sigma_{pq} \Big ( u_p^{(1)} + u_p^{(2)} \Big ) \Big ( w_q^{(1)} + w_q^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{4} + \sum_{p,q=x,y,z} \sigma_{pq} \Big ( u_p^{(1)} + u_p^{(2)} \Big ) \Big ( w_q^{(1)} + w_q^{(2)} \Big ) + \end{align} + :label: inner_product_anisotropic_faces_1 + +where superscripts :math:`(1)` and :math:`(2)` denote face 1 and face 2, respectively. +Using the contribution for each cell described in expression :eq:`inner_product_anisotropic_faces_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_anisotropic_faces`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_f}` which projects quantities on the x, y and z faces separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell faces as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_z} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{\boldsymbol{u} \, M_{\Sigma f}} \, \boldsymbol{w} + +The inner product matrix defined in the previous expression is given by: + +.. math:: + \boldsymbol{M_{\Sigma f}} = \frac{1}{4} \boldsymbol{P_f^T Q_u^T} \textrm{diag} \boldsymbol{\big ( (e_k \otimes e_k \otimes v) \odot \sigma \big )} \boldsymbol{Q_w P_f} + +where :math:`\boldsymbol{\sigma}` is a large vector that organizes vectors :math:`\boldsymbol{\sigma_{pq}}` for :math:`p,q=x,y,z` as: + +.. math:: + \boldsymbol{\sigma} = \begin{bmatrix} + \boldsymbol{\sigma_{xx}} , \; \boldsymbol{\sigma_{xy}} , \; \boldsymbol{\sigma_{xz}} , \; + \boldsymbol{\sigma_{yx}} , \; \boldsymbol{\sigma_{yy}} , \; \boldsymbol{\sigma_{yz}} , \; + \boldsymbol{\sigma_{zx}} , \; \boldsymbol{\sigma_{zy}} , \; \boldsymbol{\sigma_{zz}} \end{bmatrix}^T + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is now a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_f}` is a projection matrix that maps quantities from faces to cell centers + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{Q_u}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z} ]^T` + - :math:`\boldsymbol{Q_w}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_z} \; \boldsymbol{u_z} ]^T` + + +Vectors on Cell Edges +^^^^^^^^^^^^^^^^^^^^^ + +We want to approximate the inner product between a vector quantity :math:`\vec{u}` and the product of +:math:`\Sigma` and :math:`\vec{w}`, where :math:`\Sigma` given in expression :eq:`inner_product_tensor`. +Here, we discretize such that :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live on cell edges. Our goal is to construct the inner product matrix :math:`\boldsymbol{M}` such that: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv \approx \boldsymbol{u^T \, M \, w} + :label: inner_product_anisotropic_edges + +where :math:`\Sigma` is defined in expression :eq:`inner_product_tensor`. +We must respect the dot product and the tensor. For vectors defined on cell edges, we discretize such that the +x-component of the vectors live on the x-edges, the y-component lives y-edges and the z-component +lives on the z-edges. This is illustrated in 2D and 3D below. By decomposing the +domain into a set of finite cells, we assume the tensor properties are spacial invariant within each cell. + +.. figure:: ../../images/edge_discretization.png + :align: center + :width: 600 + +As we can see there are 2 edges for each component in 2D and 4 edges for each component in 3D. +Therefore, we need to project each component of the vector from its edges to the cell centers and take their averages separately. +Since the tensor is symmetric, it has 3 independent components in 2D and 6 independent components in 3D. +Using this, we can reduce the size of the computation. + +For a single cell :math:`i` with volume :math:`v` and tensor properties defined by +:math:`\sigma_{pq}` for :math:`p,q=x,y,z`, +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{4} + \sum_{p,q=x,y} \sigma_{pq} \Big ( u_p^{(1)} + u_p^{(2)} \Big ) \Big ( w_q^{(1)} + w_q^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v}{16} + \sum_{p,q=x,y,z} \sigma_{pq} \Big ( u_p^{(1)} + u_p^{(2)} + u_p^{(3)} + u_p^{(4)} \Big ) + \Big ( w_q^{(1)} + w_q^{(2)} + w_q^{(3)} + w_q^{(4)} \Big ) + \end{align} + :label: inner_product_anisotropic_edges_1 + +where the superscripts :math:`(1)` to :math:`(4)` denote a particular edges. +Using the contribution for each cell described in expression :eq:`inner_product_anisotropic_edges_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_anisotropic_edges`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_e}` which projects quantities on the x, y and z edges separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell edges as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \Sigma \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{\boldsymbol{u} \, M_{\Sigma e} \, \boldsymbol{w}} + +The inner product matrix defined in the previous expression is given by: + +.. math:: + \boldsymbol{M_{\Sigma e}} = \frac{1}{4^{k-1}} \boldsymbol{P_e^T Q_u^T} \textrm{diag} \boldsymbol{\big ( (e_k \otimes e_k \otimes v) \odot \sigma \big )} \boldsymbol{Q_w P_e} + +where :math:`\boldsymbol{\sigma}` is a large vector that organizes vectors :math:`\boldsymbol{\sigma_{pq}}` for :math:`p,q=x,y,z` as: + +.. math:: + \boldsymbol{\sigma} = \begin{bmatrix} + \boldsymbol{\sigma_{xx}} , \; \boldsymbol{\sigma_{xy}} , \; \boldsymbol{\sigma_{xz}} , \; + \boldsymbol{\sigma_{yx}} , \; \boldsymbol{\sigma_{yy}} , \; \boldsymbol{\sigma_{yz}} , \; + \boldsymbol{\sigma_{zx}} , \; \boldsymbol{\sigma_{zy}} , \; \boldsymbol{\sigma_{zz}} \end{bmatrix}^T + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_e}` is a projection matrix that maps quantities from edges to cell centers + - :math:`\boldsymbol{Q_u}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z} ]^T` + - :math:`\boldsymbol{Q_w}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_z} \; \boldsymbol{u_z} ]^T` + + +.. _inner_products_anisotropic_reciprocal: + +Reciprocal Properties +--------------------- + +Let :math:`\vec{v}` and :math:`\vec{w}` be two physically related quantities. +If their relationship is anisotropic and defined in terms of the reciprocal of a property (defined by a tensor :math:`\Gamma`), the constitutive relation is of the form: + +.. math:: + \vec{v} = \Gamma \vec{w} + :label: inner_product_anisotropic_reciprocal + +where + +.. math:: + \mathbf{In \; 2D:} \; + \Gamma = \begin{bmatrix} \rho^{-1}_{xx} & \rho^{-1}_{xy} \\ + \rho^{-1}_{yx} & \rho^{-1}_{yy} \end{bmatrix} + \;\;\;\;\;\;\;\; \mathbf{In \; 3D:} \; + \Sigma = \begin{bmatrix} \rho^{-1}_{xx} & \rho^{-1}_{xy} & \rho^{-1}_{xz} \\ + \rho^{-1}_{yx} & \rho^{-1}_{yy} & \rho^{-1}_{yz} \\ + \rho^{-1}_{zx} & \rho^{-1}_{zy} & \rho^{-1}_{zz} \end{bmatrix} + +Note that for real materials, the tensor is symmetric and has 6 independent variables +(i.e. :math:`\rho_{pq}=\rho_{qp}` for :math:`p,q=x,y,z`). + +**Diagonal Anisotropic:** + +For the diagonal anisotropic case, the tensor characterzing the material properties +has the form: + +.. math:: + \mathbf{In \; 2D:} \; + \Gamma = \begin{bmatrix} \rho^{-1}_{x} & 0 \\ + 0 & \rho^{-1}_{y} \end{bmatrix} + \;\;\;\;\;\;\;\; \mathbf{In \; 3D:} \; + \Sigma = \begin{bmatrix} \rho^{-1}_{x} & 0 & 0 \\ + 0 & \rho^{-1}_{y} & 0 \\ + 0 & 0 & \rho^{-1}_{z} \end{bmatrix} + :label: inner_product_tensor_diagonal_reciprocal + + +The inner product matrix defined in expression :eq:`inner_product_anisotropic_general` is given by: + +.. math:: + \textrm{Vectors on faces:} \; \boldsymbol{M_{\Gamma f}} &= \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \rho^{-1} \big )} \boldsymbol{P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_{\Gamma e}} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \rho^{-1} \big )} \boldsymbol{P_e} + +where :math:`\boldsymbol{\rho^{-1}}` organizes vectors :math:`\boldsymbol{\rho^{-1}_x}`, +:math:`\boldsymbol{\rho^{-1}_y}` and :math:`\boldsymbol{\rho^{-1}_z}` as: + +.. math:: + \boldsymbol{\rho^{-1}} = \begin{bmatrix} \boldsymbol{\rho^{-1}_x} \\ \boldsymbol{\rho^{-1}_y} \\ \boldsymbol{\rho^{-1}_z} \\ \end{bmatrix} + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from faces and edges to cell centers, respectively + +**Fully Anisotropic:** + +For a fully anisotropic case, the tensor characterizing the material properties +has the form is given by: + +.. math:: + \mathbf{In \; 2D:} \; + \Gamma = \begin{bmatrix} \rho^{-1}_{xx} & \rho^{-1}_{xy} \\ + \rho^{-1}_{yx} & \rho^{-1}_{yy} \end{bmatrix} + \;\;\;\;\;\;\;\; \mathbf{In \; 3D:} \; + \Gamma = \begin{bmatrix} \rho^{-1}_{xx} & \rho^{-1}_{xy} & \rho^{-1}_{xz} \\ + \rho^{-1}_{yx} & \rho^{-1}_{yy} & \rho^{-1}_{yz} \\ + \rho^{-1}_{zx} & \rho^{-1}_{zy} & \rho^{-1}_{zz} \end{bmatrix} + :label: inner_product_tensor_reciprocal + +The inner product matrix defined in expression :eq:`inner_product_anisotropic_general` is given by: + +.. math:: + \textrm{Vectors on faces:} \; \boldsymbol{M_{\Gamma f}} &= \frac{1}{4} \boldsymbol{P_f^T Q_u^T} \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \rho^{-1} \big )} \boldsymbol{Q_w P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_{\Gamma e}} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T Q_u^T} \textrm{diag} \boldsymbol{\big ( (e_k \otimes v) \odot \rho^{-1} \big )} \boldsymbol{Q_w P_e} + +where :math:`\boldsymbol{\rho^{-1}}` is a large vector that organizes vectors :math:`\boldsymbol{\rho^{-1}_{pq}}` for :math:`p,q=x,y,z` as: + +.. math:: + \boldsymbol{\Gamma} = \begin{bmatrix} + \boldsymbol{\rho^{-1}_{xx}} , \; \boldsymbol{\rho^{-1}_{xy}} , \; \boldsymbol{\rho^{-1}_{xz}} , \; + \boldsymbol{\rho^{-1}_{yx}} , \; \boldsymbol{\rho^{-1}_{yy}} , \; \boldsymbol{\rho^{-1}_{yz}} , \; + \boldsymbol{\rho^{-1}_{zx}} , \; \boldsymbol{\rho^{-1}_{zy}} , \; \boldsymbol{\rho^{-1}_{zz}} \end{bmatrix}^T + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from faces and edges to cell centers, respectively + - :math:`\boldsymbol{Q_u}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z} ]^T` + - :math:`\boldsymbol{Q_w}` is a sparse replication matrix that augments a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}]^T` to create a vector of the form :math:`[\boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_x}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_y}, \; \boldsymbol{u_z}, \; \boldsymbol{u_z} \; \boldsymbol{u_z} ]^T` \ No newline at end of file diff --git a/docs/content/theory/inner_products_basic.rst b/docs/content/theory/inner_products_basic.rst new file mode 100644 index 000000000..ac7bbf5c4 --- /dev/null +++ b/docs/content/theory/inner_products_basic.rst @@ -0,0 +1,308 @@ +.. _inner_products_basic: + +Basic Inner Products +******************** + +Summary +------- + +Inner products between two scalar or vector quantities represents the most +basic class of inner products. For scalar quantities :math:`\psi` and :math:`\phi`, +we will show that a disrete approximation to the inner product is given by: + +.. math:: + (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv \approx \boldsymbol{\psi^T M \, \phi} + :label: inner_product_basic_scalar + +And for vector quantities :math:`\vec{u}` and :math:`\vec{w}`, a discrete approximation +to the inner product is given by: + +.. math:: + (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{u^T M \, w} + :label: inner_product_basic_vector + +where :math:`\boldsymbol{M}` in either equation represents an +*inner-product matrix*, and :math:`\boldsymbol{\psi}`, :math:`\boldsymbol{\phi}`, +:math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are discrete variables that live +on the mesh. It is important to note a few things about the +inner-product matrix in this case: + + 1. It depends on the dimensions and discretization of the mesh + 2. It depends on where the discrete variables live; e.g. edges, faces, nodes, centers + +For this simple class of inner products, the corresponding form of the inner product matricies for +discrete quantities living on various parts of the mesh are shown below: + +.. math:: + \textrm{Scalars at centers:} \; \boldsymbol{M_c} &= \textrm{diag} (\boldsymbol{v} ) \\ + \textrm{Scalars at nodes:} \; \boldsymbol{M_n} &= \frac{1}{2^{2k}} \boldsymbol{P_n^T } \textrm{diag} (\boldsymbol{v} ) \boldsymbol{P_n} \\ + \textrm{Vectors on faces:} \; \boldsymbol{M_f} &= \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} (\boldsymbol{e_k \otimes v} ) \boldsymbol{P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_e} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} (\boldsymbol{e_k \otimes v}) \boldsymbol{P_e} + +where + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_n}`, :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from nodes, faces and edges to cell centers, respectively + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + +**Tutorial:** To construct the inner product matrix and/or approximate inner products of this type, see the :ref:`tutorial on basic inner products ` + + +Scalars at Cell Centers +----------------------- + +We want to approximate the inner product of two scalar quantities :math:`\psi` and :math:`\phi` +where the discrete quantities :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{\phi}` are defined +to live at cell centers. Assuming we know the values of :math:`\psi` and :math:`\phi` at cell centers, +our goal is to construct the inner product matrix :math:`\boldsymbol{M}` such that: + +.. math:: + (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv \approx \boldsymbol{\psi^T M \, \phi} + + +.. figure:: ../../images/center_discretization.png + :align: center + :width: 400 + + A single 2D cell (left) and a single 3D cell (right). + + +For a single cell (see above), the contribution towards the inner product is obtained by multiplying +:math:`\psi` and :math:`\phi` at the cell center (represented by :math:`\psi_i` and :math:`\phi_i`) +by the cell's volume (:math:`v_i`), i.e.: + +.. math:: + \int_{\Omega_i} \psi \, \phi \, dv \approx \psi_i \phi_i v_i + +Therefore a simple approximation to the inner product is obtained by summing the above +approximation over all cells. Where :math:`nc` refers to the number of cells in the mesh: + +.. math:: + \int_\Omega \psi \, \phi \, dv \approx \sum_i^{nc} \psi_i \phi_i v_i + +Expressing the sum in terms of linear equations, we obtain: + +.. math:: + (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv \approx \boldsymbol{\psi^T M_c \, \phi} + +where the mass matrix for cell centered quantities is just a diagonal matrix containing +the cell volumes (:math:`\boldsymbol{v}`), i.e.: + +.. math:: + \boldsymbol{M_c} = diag(\boldsymbol{v}) + + +Scalars at Nodes +---------------- + +We want to approximate the inner product of two scalar quantities :math:`\psi` and :math:`\phi` +where the discrete quantities :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{\phi}` are defined +to live on cell nodes. Assuming we know the values of :math:`\psi` and :math:`\phi` at the nodes, +our goal is to construct the inner product matrix :math:`\boldsymbol{M}` such that: + +.. math:: + (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv \approx \boldsymbol{\psi^T M \, \phi} + :label: inner_product_basic_nodes + +Whereas :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{\phi}` are defined +to live on cell nodes, it makes more sense for cell volumes to be considered a property +which lives at cell centers. This makes evaluating the inner product more complicated as +discrete quantities do not live at the same place. + +.. figure:: ../../images/node_discretization.png + :align: center + :width: 600 + + Illustration for approximating the inner product for nodal quantities. + +For a single cell :math:`i`, the contribution towards the inner product is approximated by +mapping the values at the nodes to cell centers, taking the average, then multiplying +by the cell volume. For 2D cells there are 4 nodes. And for 3D cells there are 8 nodes +Thus: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \psi \, \phi \, dv \approx & \;\; + \frac{v_i}{16} \Bigg ( \psi_i^{(1)} \! + \psi_i^{(2)} \! + \psi_i^{(3)} \! + \psi_i^{(4)} \Bigg ) + \Bigg ( \phi_i^{(n1)} \! + \phi_i^{(n2)} \! + \phi_i^{(n3)} \! + \phi_i^{(n4)} \Bigg ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \psi \, \phi \, dv \approx & \;\; + \frac{v_i}{64} \Bigg ( \sum_{n=1}^8 \psi_i^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^8 \psi_i^{(n)} \Bigg ) + \end{align} + :label: inner_product_basic_nodes_1 + +where the superscript :math:`(n)` is used to point to a specific node. +Using the contribution for each cell described in expression :eq:`inner_product_basic_nodes_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_basic_nodes`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_n}` which projects quantities on the nodes to the +the cell centers. + +Our final approximation for the inner product is therefore: + +.. math:: + (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv \approx \boldsymbol{\psi^T M_n \, \phi} + +where the mass matrix for nodal quantities has the form: + +.. math:: + \boldsymbol{M_n} = \frac{1}{2^{2k}} \boldsymbol{P_n^T } \textrm{diag} (\boldsymbol{v} ) \boldsymbol{P_n} + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{P_n}` is a projection matrix that maps quantities from nodes to cell centers + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + + +Vectors on Cell Faces +--------------------- + +For the mimetic finite volume approach, fluxes are generally defined on cell faces; +as it allows cells to share faces while preserving natural boundary conditions. + +We want to approximate the inner product of two vector quantities :math:`\vec{u}` and :math:`\vec{w}` +where the discrete quantities :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live on cell faces. Assuming we know the values of :math:`\vec{u}` and :math:`\vec{w}` on the faces, +our goal is to construct the inner product matrix :math:`\boldsymbol{M}` in the expression below: + +.. math:: + (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{u^T M \, w} + :label: inner_product_basic_faces + +We must respect the dot product. For vectors defined on cell faces, we discretize such that the +x-component of the vectors live on the x-faces, the y-component lives y-faces and the z-component +lives on the z-faces. For a single cell, this is illustrated in 2D and 3D below. + +.. figure:: ../../images/face_discretization.png + :align: center + :width: 600 + + Illustration for approximating the inner product for vector quantities living on faces. + + +As we can see there are 2 faces for each component. Therefore, we need to project each component of the +vector from its faces to the cell centers and take their averages separately. For a single cell with volume :math:`v_i`, +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i}{4} \Big ( u_x^{(1)} + u_x^{(2)} \Big ) \Big ( w_x^{(1)} + w_x^{(2)} \Big ) \\ + & + \frac{v_i}{4} \Big ( u_y^{(1)} + u_y^{(2)} \Big ) \Big ( w_y^{(1)} + w_y^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i}{4} \Big ( u_x^{(1)} + u_x^{(2)} \Big ) \Big ( w_x^{(1)} + w_x^{(2)} \Big ) \\ + & + \frac{v_i}{4} \Big ( u_y^{(1)} + u_y^{(2)} \Big ) \Big ( w_y^{(1)} + w_y^{(2)} \Big ) \\ + & + \frac{v_i}{4} \Big ( u_z^{(1)} + u_z^{(2)} \Big ) \Big ( w_z^{(1)} + w_z^{(2)} \Big ) + \end{align} + :label: inner_product_basic_faces_1 + +where superscripts :math:`(1)` and :math:`(2)` denote face 1 and face 2, respectively. +Using the contribution for each cell described in expression :eq:`inner_product_basic_faces_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_basic_faces`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_f}` which projects quantities on the x, y and z faces separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell faces as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{u^T M_f \, w} + +where the mass matrix for face quantities has the form: + +.. math:: + \boldsymbol{M_f} = \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} (\boldsymbol{e_k \otimes v} ) \boldsymbol{P_f} + +and + + - :math:`k = 1,2,3` represents the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_f}` is the projection matrix that maps quantities from faces to cell centers + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + + +Vectors on Cell Edges +--------------------- + +For the mimetic finite volume approach, fields are generally defined on cell edges; +as it allows cells to share edges while preserving natural boundary conditions. +We want to approximate the inner product of two vector quantities :math:`\vec{u}` and :math:`\vec{w}` +where the discrete quantities :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live at cell edges. Assuming we know the values of :math:`\vec{u}` and :math:`\vec{w}` at the edges, +our goal is to construct the inner product matrix :math:`\boldsymbol{M}` in the expression below: + +.. math:: + (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{u^T M \, w} + :label: inner_product_basic_edges + +We must respect the dot product. For vectors defined on cell edges, we discretize such that the +x-component of the vectors live on the x-edges, the y-component lives y-edges and the z-component +lives on the z-edges. This is illustrated in 2D and 3D below. + +.. figure:: ../../images/edge_discretization.png + :align: center + :width: 600 + + Illustration for approximating the inner product for vector quantities living on edges. + + +As we can see there are 2 edges for each component in 2D and 4 edges for each component in 3D. +Therefore, we need to project each component of the +vector from its edges to the cell centers and take their averages separately. For a single cell with volume :math:`v_i`, +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i}{4} \Big ( u_x^{(1)} + u_x^{(2)} \Big ) \Big ( w_x^{(1)} + w_x^{(2)} \Big ) \\ + & + \frac{v_i}{4} \Big ( u_y^{(1)} + u_y^{(2)} \Big ) \Big ( w_y^{(1)} + w_y^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i}{16} \Bigg ( \sum_{n=1}^4 u_x^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^4 w_x^{(n)} \Bigg ) \\ + & + \frac{v_i}{16} \Bigg ( \sum_{n=1}^4 u_y^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^4 w_y^{(n)} \Bigg ) \\ + & + \frac{v_i}{16} \Bigg ( \sum_{n=1}^4 u_z^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^4 w_z^{(n)} \Bigg ) + \end{align} + :label: inner_product_basic_edges_1 + +where the superscript :math:`(n)` denotes a particular edge. +Using the contribution for each cell described in expression :eq:`inner_product_basic_edges_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_basic_edges`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_e}` which projects quantities on the x, y and z edges separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell edges as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{\u^T M_e \, w} + +where the mass matrix for face quantities has the form: + +.. math:: + \boldsymbol{M_e} = \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} (\boldsymbol{e_k \otimes v}) \boldsymbol{P_e} + +and + + - :math:`k = 1,2,3` represents the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_e}` is the projection matrix that maps quantities from edges to cell centers + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + diff --git a/docs/content/theory/inner_products_boundary_conditions.rst b/docs/content/theory/inner_products_boundary_conditions.rst new file mode 100644 index 000000000..4bd95c2a9 --- /dev/null +++ b/docs/content/theory/inner_products_boundary_conditions.rst @@ -0,0 +1,668 @@ +.. _inner_products_boundary_conditions: + +Boundary Conditions +******************* + +Introduction +------------ + +In :ref:`inner products containing differential operators `, +we approximated inner products containing the gradient, the divergence and the curl. +If our choice in discretization does not respect the boundary conditions naturally, +we must construct a discrete approximation to a surface integral to impose the boundary conditions. +Here, we demonstrate how the boundary conditions are implemented discretely. + +.. _inner_products_boundary_conditions_gradient: + +Gradient +-------- + +Let :math:`\phi` be a scalar quantity and let :math:`\vec{u}` be a vector quantity. +If the discrete representation :math:`\boldsymbol{\phi}` lives at cell centers and +the discrete representation :math:`\boldsymbol{u}` lives on the faces, +the inner product is approximated by: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T D^T M_c \, \phi} + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da + :label: inner_products_boundary_conditions_1 + +where + + - :math:`\boldsymbol{D}` is a :ref:`discrete divergence operator ` + - :math:`\boldsymbol{M_c}` is the :ref:`basic inner product matrix for quantities at cell centers ` + +.. note:: To see how equation :eq:`inner_products_boundary_conditions_1` was obtained, visit :ref:`inner products with gradients `. + +.. figure:: ../../images/boundary_conditions_gradient.png + :align: center + :width: 300 + + Figure 1: Boundary conditions for the gradient operator. + +A discrete approximation to the surface integral in equation +:eq:`inner_products_boundary_conditions_1` which imposes +boundary conditions on :math:`\phi` can be expressed as: + +.. math:: + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da \approx \boldsymbol{u^T} diag(\boldsymbol{\tilde{a}}) \,\boldsymbol{\tilde{\phi}} + :label: inner_products_boundary_conditions_1a + +where + + - :math:`\boldsymbol{u^T}` is the discrete representation of :math:`\vec{u}` on the faces + - :math:`\boldsymbol{\tilde{\phi}}` is a discrete vector on the faces which contains the values of :math:`\phi` approximated on boundary faces + - :math:`\boldsymbol{\tilde{a}}` is a vector which stores the dot products between the surface area vectors of the boundary faces :math:`\vec{a}` and the unit direction :math:`\hat{u} = \frac{\vec{u}}{|\vec{u}|}`. That is, :math:`\boldsymbol{\tilde{a}} = \hat{u} \cdot \vec{a}` for faces on the boundary and :math:`\boldsymbol{\tilde{a}}=0` for all interior faces. + +Here, we describe how the vector :math:`\boldsymbol{\tilde{\phi}}` can be constructed to impose various boundary conditions. + + +Dirichlet +^^^^^^^^^ + +Examine :ref:`Figure 1 ` and consider face :math:`k`. +When imposing Dirichlet conditions, we know the value of :math:`\phi` on the boundary +and we can construct :math:`\boldsymbol{\tilde{\phi}}` directly. +Let :math:`\boldsymbol{\tilde{\phi}}` be a discrete vector defined on the faces of the mesh +such that :math:`\boldsymbol{\tilde{\phi}} = \phi_k` for each face :math:`k` on the boundary +and :math:`\boldsymbol{\tilde{\phi}} = 0` for all interior faces. + +To implement the boundary condition practically, +we construct a vector :math:`\boldsymbol{b} = diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{\phi}}` +that lives on the faces. For equation :eq:`inner_products_boundary_conditions_1` in the case of +Dirichlet boundary conditions we obtain: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T D^T M_c \, \phi + u^T b} + :label: inner_products_boundary_conditions_1b + + +Neumann +^^^^^^^ + +According to equation :eq:`inner_products_boundary_conditions_1a`, we need to approximate :math:`\phi` on +the boundary faces when the derivative of :math:`\phi` normal to the surface is known. +For each face, we will approximate :math:`\phi` using a Taylor expansion. + +Examine :ref:`Figure 1 `. +We let :math:`\phi_k` denote the value of :math:`\phi` on face :math:`k`, +we let :math:`\phi_c` denote the value of :math:`\phi` at the nearest cell center, +and :math:`\Delta s_k` denotes the path from from face :math:`k` to the cell center. +Where the derivative of :math:`\phi` normal to the surface for face :math:`k` is denoted by :math:`\frac{\partial \phi_k}{\partial n}`, +we can define the following Taylor expansion: + +.. math:: + \phi_c = \phi_k + \frac{\partial \phi_k}{\partial n} \Delta s_k \; + \; ... + +If we rearrange this equation, we can use the value at the center of the cell and the Neumann condition on the boundary +to approximate :math:`\phi` on the corresponding face, i.e.: + +.. math:: + \phi_k \approx \phi_c - \frac{\partial \phi_k}{\partial n} \Delta s_k + +The same approach can be used to approximate :math:`\phi` on the other boundary faces of the mesh. +In this case, :math:`\boldsymbol{\tilde{\phi}}` in equation :eq:`inner_products_boundary_conditions_1a` can be expressed as: + +.. math:: + \boldsymbol{\tilde{\phi}} = \boldsymbol{P_{cf} \phi + \tilde{b}} \;\;\;\; + \textrm{where} \;\; \boldsymbol{\tilde{b}} = \begin{cases} + - \frac{\partial \phi_k}{\partial n} \Delta s_k \;\; \textrm{on boundary faces} \\ + 0 \;\; \textrm{on interior faces} + \end{cases} + :label: inner_products_boundary_conditions_1c + +where :math:`\boldsymbol{P_{cf}}` is a projection matrix from cell centers to faces and :math:`\boldsymbol{\tilde{b}}` is a vector that lives on the faces. +If we substitute equation :eq:`inner_products_boundary_conditions_1c` into equation :eq:`inner_products_boundary_conditions_1a`, +the surface integral is approximated by: + +.. math:: + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da \approx \boldsymbol{u^T} \, diag(\boldsymbol{\tilde{a}}) \, \big ( \boldsymbol{P_{cf} \phi + \tilde{b}} \big ) + :label: inner_products_boundary_conditions_1d + +To implement the result in equation :eq:`inner_products_boundary_conditions_1d` practically, +let us construct +a matrix :math:`\boldsymbol{B} = diag(\boldsymbol{\tilde{a}}) \boldsymbol{P_{cf}}` and +a vector :math:`\boldsymbol{b} = diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{b}}` +that lives on the faces. For equation :eq:`inner_products_boundary_conditions_1` in the case of +Neumann boundary conditions we obtain: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T (D^T M_c - B) \, \phi + u^T b} + :label: inner_products_boundary_conditions_1e + +Mixed +^^^^^ + +Where :math:`\alpha`, :math:`\beta` and :math:`\gamma` are constants, +the boundary conditions are defined by the following ordinary differential equation: + +.. math:: + \alpha \phi + \beta \frac{\partial \phi}{\partial n} = \gamma + :label: inner_products_boundary_conditions_1f + +Examine :ref:`Figure 1 `. +We let :math:`\phi_k` denote the value of :math:`\phi` on face :math:`k`, +we let :math:`\phi_c` denote the value of :math:`\phi` at the nearest cell center, +and :math:`\Delta s_k` denotes the path from from face :math:`k` to the cell center. +Where the derivative of :math:`\phi` normal to the surface for face :math:`k` is denoted by :math:`\frac{\partial \phi_k}{\partial n}`, +we can define the following Taylor expansion: + +.. math:: + \phi_c = \phi_k + \frac{\partial \phi_k}{\partial n} \Delta s_k \; + \; ... + :label: inner_products_boundary_conditions_1g + +A first order approximation to the derivative on the boundary is given by: + +.. math:: + \frac{\partial \phi_k}{\partial n} \approx \frac{\phi_c - \phi_k}{\Delta s_k} + :label: inner_products_boundary_conditions_1h + +Substituting equations :eq:`inner_products_boundary_conditions_1g` and :eq:`inner_products_boundary_conditions_1h` into equation :eq:`inner_products_boundary_conditions_1f` for face :math:`k` we obtain: + +.. math:: + \phi_k = \Bigg ( \frac{\beta}{\beta - \alpha \Delta s_k} \Bigg ) \phi_c - \Bigg ( \frac{\gamma \Delta s_k}{\beta - \alpha \Delta s_k} \Bigg ) + :label: inner_products_boundary_conditions_1i + +The same approach can be used to approximate :math:`\phi` on the other boundary faces of the mesh. + +Let :math:`\boldsymbol{\tilde{b}_1}` be a discrete vector defined on the faces of the mesh such that :math:`\boldsymbol{\tilde{b_1}} = \frac{\beta}{\beta - \alpha \Delta s_k}` for faces on the boundary +and :math:`\boldsymbol{\tilde{b}_1} = 0` for all internal faces. +And let :math:`\boldsymbol{\tilde{b}_2}` be a discrete vector defined on the faces of the mesh +such that :math:`\boldsymbol{\tilde{b}_2} = - \frac{\gamma \Delta s_k}{\beta - \alpha \Delta s_k}` for faces on the boundary +and :math:`\boldsymbol{\tilde{b}_2} = 0` for all internal faces. +In this case, we define the following vector :math:`\boldsymbol{\tilde{\phi}}` that lives on cell faces: + +.. math:: + \boldsymbol{\tilde{\phi}} = diag (\boldsymbol{\tilde{b}_1}) \, \boldsymbol{P_{cf} \, \phi + \tilde{b}_2} + :label: inner_products_boundary_conditions_1j + +where :math:`\boldsymbol{P_{cf}}` is a projection matrix from cell centers to faces. +If we substitute equation :eq:`inner_products_boundary_conditions_1j` into equation :eq:`inner_products_boundary_conditions_1a`, +the surface integral is approximated by: + +.. math:: + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da \approx \boldsymbol{u^T} \, diag(\boldsymbol{\tilde{a}}) \, \big ( diag (\boldsymbol{\tilde{b}_1}) \, \boldsymbol{P_{cf} \phi + \tilde{b}_2} \big ) + :label: inner_products_boundary_conditions_1k + +To implement the result in equation :eq:`inner_products_boundary_conditions_1k` practically, +let us construct a matrix :math:`\boldsymbol{B} = diag \big ( \boldsymbol{\tilde{a} \odot \tilde{b}_1} \big ) \, \boldsymbol{P_{cf}}` and +a vector :math:`\boldsymbol{b} = diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{b}_2}` +that lives on the faces. For equation :eq:`inner_products_boundary_conditions_1` in the case of +Mixed boundary conditions we obtain: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T (D^T M_c - B) \, \phi + u^T b} + :label: inner_products_boundary_conditions_1l + + +.. _inner_products_boundary_conditions_divergence: + +Divergence +---------- + +Let :math:`\psi` be a scalar quantity and let :math:`\vec{w}` be a vector quantity. +If the discrete representation :math:`\boldsymbol{\psi}` lives on the nodes and +the discrete representation :math:`\boldsymbol{w}` lives on the edges, +the inner product is approximated by: + +.. math:: + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx - \boldsymbol{\psi^T G^T M_e \, w} + \oint_{\partial \Omega} \psi (\hat{n} \cdot \vec{w}) \, da + :label: inner_products_boundary_conditions_2 + +where + + - :math:`\boldsymbol{G}` is a :ref:`discrete gradient operator ` + - :math:`\boldsymbol{M_e}` is the :ref:`basic inner product matrix for vectors at edges ` + +.. note:: To see how equation :eq:`inner_products_boundary_conditions_2` was obtained, visit :ref:`inner products with the divergence `. + +.. figure:: ../../images/boundary_conditions_divergence.png + :align: center + :width: 300 + + Figure 2: Boundary conditions for the divergence operator. + +A discrete approximation to the surface integral in equation +:eq:`inner_products_boundary_conditions_2` which imposes +boundary conditions on :math:`\vec{w}` can be expressed as: + +.. math:: + \oint_{\partial \Omega} \psi (\hat{n} \cdot \vec{w}) \, da \approx \boldsymbol{\psi^T P_{nf}^T} \, diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{w}} + :label: inner_products_boundary_conditions_2a + +where + + - :math:`\boldsymbol{\psi^T}` is the discrete representation of :math:`\psi` on the nodes + - :math:`\boldsymbol{P_{nf}}` is sparse matrix that projects scalar quantities from nodes to faces + - :math:`\boldsymbol{\tilde{a}}` is a vector which stores the dot products between the surface area vectors of the boundary faces :math:`\vec{a}` and the unit direction :math:`\hat{u} = \frac{\vec{u}}{|\vec{u}|}`. That is, :math:`\boldsymbol{\tilde{a}} = \hat{u} \cdot \vec{a}` for faces on the boundary and :math:`\boldsymbol{\tilde{a}}=0` for all interior faces. + - :math:`\boldsymbol{\tilde{w}}` is a discrete vector which contains the x, y (and z) components of :math:`\vec{w}` approximated on boundary faces; i.e. :math:`\tilde{w}_x`, :math:`\tilde{w}_y` (and :math:`\tilde{w}_z`). + +Here, we describe how the vector :math:`\boldsymbol{\tilde{w}}` can be constructed to impose various boundary conditions. + +Dirichlet +^^^^^^^^^ + +Examine :ref:`Figure 1 `. +In the case of the Dirichlet condition, we know the value of :math:`\vec{w}` on the boundary +and we can construct :math:`\boldsymbol{\tilde{w}}` directly. +Let :math:`\boldsymbol{\tilde{w}}` be a discrete vector defined on the faces of the mesh +which organizes the x, y (and z components) as follows: + +.. math:: + \boldsymbol{\tilde{w}} = \begin{bmatrix} + \boldsymbol{\tilde{w}_x} \\ \boldsymbol{\tilde{w}_y} \\ \boldsymbol{\tilde{w}_z} \end{bmatrix} + +For x-faces on the x-boundary, the entries of :math:`\boldsymbol{\tilde{w}_x}` equal the corresponding x-component of :math:`\vec{w}`. +And on all other x-faces, the entries are equal to zero. +This is similar for :math:`\boldsymbol{\tilde{w}_y}` and :math:`\boldsymbol{\tilde{w}_z}` which only have non-zero entries on y-faces +and z-faces, respectively. + +To implement the boundary condition practically, +let us construct a vector :math:`\boldsymbol{b} = \boldsymbol{P_{nf}^T} \, diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{w}}` +that utlimately lives on the nodes. For equation :eq:`inner_products_boundary_conditions_2` in the case of +Dirichlet boundary conditions we obtain: + +.. math:: + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx - \boldsymbol{\psi^T G^T M_e \, w + \psi^T b} + :label: inner_products_boundary_conditions_2b + + +Neumann +^^^^^^^ + +According to equation :eq:`inner_products_boundary_conditions_2a`, we need to construct the dicrete face vector +:math:`\boldsymbol{\tilde{w}}` when the derivative normal to the surface is known. + +Now examine :ref:`Figure 2 `. +Since the discrete vector :math:`\boldsymbol{w}` is lives on the edges, +we start by defining a Taylor expansion at node :math:`k`. +We let :math:`w_k` denote the approximation of the x component of :math:`\vec{w}` on node :math:`k`, +we let :math:`w_x` denote the x component of :math:`\vec{w}` at the nearest x-edge, +and :math:`\Delta s_k` denotes the path from node :math:`k` to that edge. + +Where the derivative of :math:`\vec{w}` normal to the surface at node :math:`k` is known and is denoted by :math:`\frac{\partial w_k}{\partial n}`, +we can define the following Taylor expansion: + +.. math:: + w_x = w_k + \frac{\partial w_k}{\partial n} \Delta s_k \; + \; ... + +If we rearrange this equation, we can use the value on the x-edge and the Neumann condition on the boundary +to approximate the component of :math:`\vec{w}` on the corresponding x-face, i.e.: + +.. math:: + w_k \approx w_x - \frac{\partial w_k}{\partial n} \Delta s_k + :label: inner_products_boundary_conditions_2b + +The same approach can be used to approximate the value on the other x-boundary faces of the mesh. +In this case: + +.. math:: + \boldsymbol{\tilde{w}_x} = \boldsymbol{P_{fx}} \big ( \boldsymbol{P_{ex} w_x + \tilde{b}_x} \big ) + \;\;\; \textrm{where} \;\;\; + \boldsymbol{\tilde{b}_x} = \begin{cases} + - \frac{\partial w_k}{\partial n} \Delta s_k \;\; \textrm{on x-boundary nodes} \\ + 0 \;\; \textrm{on all other nodes} + \end{cases} + :label: inner_products_boundary_conditions_2c + +and where :math:`\boldsymbol{P_{fx}}` is a projection matrix from nodes to x-faces, :math:`\boldsymbol{P_{ex}}` is a projection matrix from x-edges to nodes, and :math:`\boldsymbol{\tilde{b}_x}` is a vector that lives on the nodes. +The same approach can be applied for y and z boundaries. + +To compute the vector :math:`\boldsymbol{w}` in equation :eq:`inner_products_boundary_conditions_2a`, +we must combine the Cartesian components. This results in the followin expression: + +.. math:: + \boldsymbol{\tilde{w}} = \boldsymbol{\tilde{P}_{nf}} \big ( \boldsymbol{\tilde{P}_{en} w + \tilde{b}} \big ) + :label: inner_products_boundary_conditions_2d + +such that: + +.. math:: + \boldsymbol{\tilde{w}} \! = \! \begin{bmatrix} \boldsymbol{\tilde{w}_x} \\ \boldsymbol{\tilde{w}_y} \\ \boldsymbol{\tilde{w}_z} \end{bmatrix} + \textrm{,}\;\; + \boldsymbol{\tilde{P}_{nf}} \! = \! \begin{bmatrix} \boldsymbol{P_{fx}} & 0 & 0 \\ 0 & \boldsymbol{P_{fy}} & 0 \\ 0 & 0 & \boldsymbol{P_{fz}} \end{bmatrix} + \textrm{,}\;\; + \boldsymbol{\tilde{P}_{en}} \! = \! \begin{bmatrix} \boldsymbol{P_{ex}} & 0 & 0 \\ 0 & \boldsymbol{P_{ey}} & 0 \\ 0 & 0 & \boldsymbol{P_{ez}} \end{bmatrix} + \;\textrm{and}\;\; + \boldsymbol{\tilde{b}} \! = \! \begin{bmatrix} \boldsymbol{\tilde{b}_x} \\ \boldsymbol{\tilde{b}_y} \\ \boldsymbol{\tilde{b}_z} \end{bmatrix} + :label: inner_products_boundary_conditions_2e + + +.. important:: It should be noted that :math:`\boldsymbol{P_{nf}}` in equation :eq:`inner_products_boundary_conditions_2a` is **not** the same as :math:`\boldsymbol{\tilde{P}_{nf}}` in equation :eq:`inner_products_boundary_conditions_2d`! Whereas :math:`\boldsymbol{P_{nf}}` maps a scalar quantity from nodes to all faces, :math:`\boldsymbol{\tilde{P}_{nf}}` maps a vector quantity whose vector components are defined on all nodes to their respective faces. + +To implement the result in equation :eq:`inner_products_boundary_conditions_2e` practically, +we expression equation :eq:`inner_products_boundary_conditions_1` in the case of +Neumann boundary conditions as: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T [D^T M_c - B] \, \phi + u^T b} + :label: inner_products_boundary_conditions_2f + +where + + - :math:`\boldsymbol{B} = \boldsymbol{P_{nf}^T} \, diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{P}_{nf}} \boldsymbol{\tilde{P}_{en}}` + - :math:`\boldsymbol{b} = \boldsymbol{P_{nf}^T} \, diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{P}_{nf}} \boldsymbol{\tilde{b}}` + +Mixed +^^^^^ + +Where :math:`\alpha`, :math:`\beta` and :math:`\gamma` are constants, +the boundary conditions are defined by the following ordinary differential equation: + +.. math:: + \alpha \phi + \beta \frac{\partial \phi}{\partial n} = \gamma + :label: inner_products_boundary_conditions_2g + +Examine :ref:`Figure 2 `. +Since the discrete vector :math:`\boldsymbol{w}` is defined on the edges, +we start by defining a Taylor expansion at node :math:`k`. +We let :math:`w_k` denote the approximation of the x component of :math:`\vec{w}` on node :math:`k`, +we let :math:`w_x` denote the x component of :math:`\vec{w}` at the nearest x-edge, +and :math:`\Delta s_k` denotes the path from from node :math:`k` to that edge. + +Where the derivative of :math:`\vec{w}` normal to the surface at node :math:`k` is known and is denoted by :math:`\frac{\partial w_k}{\partial n}`, +we can define the following Taylor expansion: + +.. math:: + w_x = w_k + \frac{\partial w_k}{\partial n} \Delta s_k \; + \; ... + :label: inner_products_boundary_conditions_2h + +A first order approximation to the derivative on the boundary is given by: + +.. math:: + \frac{\partial w_k}{\partial n} \approx \frac{w_x - w_k}{\Delta s_k} + :label: inner_products_boundary_conditions_2i + +Substituting equations :eq:`inner_products_boundary_conditions_2h` and :eq:`inner_products_boundary_conditions_2i` into equation :eq:`inner_products_boundary_conditions_2g` for face :math:`k` we obtain: + +.. math:: + w_k = \Bigg ( \frac{\beta}{\beta - \alpha \Delta s_k} \Bigg ) w_x - \Bigg ( \frac{\gamma \Delta s_k}{\beta - \alpha \Delta s_k} \Bigg ) + :label: inner_products_boundary_conditions_2j + +Let :math:`\boldsymbol{\tilde{w}_x}` be a vector defined on the nodes of the mesh such that :math:`\boldsymbol{\tilde{w}_x}` equals the approximation :math:`\tilde{w}_x` for nodes touching the x boundary and :math:`\boldsymbol{\tilde{w}_x}` equals zero for all other nodes. +This vector can be computed according to the following expression: + +.. math:: + \boldsymbol{\tilde{w}_x} = \boldsymbol{P_{fx}} \big ( diag ( \boldsymbol{\tilde{c}_x} ) \, \boldsymbol{P_{ex} w_x + \tilde{d}_x} \big ) + :label: inner_products_boundary_conditions_2k + +where :math:`\boldsymbol{P_{fx}}` and :math:`\boldsymbol{P_{ex}}` are projection matrices that were defined in equation :eq:`inner_products_boundary_conditions_2c`. +Let :math:`\boldsymbol{\tilde{c}_x}` be a discrete vector defined on the faces of the mesh such that: + +.. math:: + \boldsymbol{\tilde{c}_x} = \begin{cases} \frac{\beta}{\beta - \alpha \Delta s_k} \;\;\textrm{on nodes on the x-boundary}\\ 0 \;\; \textrm{on all other nodes} \end{cases} + +And let :math:`\boldsymbol{\tilde{d}_x}` be a discrete vector defined on the nodes of the mesh such that: + +.. math:: + \boldsymbol{\tilde{d}_x} = \begin{cases} - \frac{\gamma \Delta s_k}{\beta - \alpha \Delta s_k} \;\;\textrm{on nodes on the x-boundary}\\ 0 \;\; \textrm{on all other nodes} \end{cases} + +The same approach can be applied for y and z boundaries. +To compute the vector :math:`\boldsymbol{w}` in equation :eq:`inner_products_boundary_conditions_2a`, +we must combine the Cartesian components. This results in the followin expression: + +.. math:: + \boldsymbol{\tilde{w}} = \boldsymbol{\tilde{P}_{nf}} \big ( diag ( \boldsymbol{\tilde{c}} ) \boldsymbol{\tilde{P}_{en} w + \tilde{d}} \big ) + :label: inner_products_boundary_conditions_2l + +where :math:`\boldsymbol{\tilde{w}}`, :math:`\boldsymbol{\tilde{P}_{nf}}` and :math:`\boldsymbol{\tilde{P}_{en}}` +were defined in equation :eq:`inner_products_boundary_conditions_2d`. And where: + +.. math:: + \boldsymbol{\tilde{c}} \! = \! \begin{bmatrix} \boldsymbol{\tilde{c}_x} \\ \boldsymbol{\tilde{c}_y} \\ \boldsymbol{\tilde{c}_z} \end{bmatrix} + \;\textrm{and}\;\; + \boldsymbol{\tilde{d}} \! = \! \begin{bmatrix} \boldsymbol{\tilde{d}_x} \\ \boldsymbol{\tilde{d}_y} \\ \boldsymbol{\tilde{d}_z} \end{bmatrix} + :label: inner_products_boundary_conditions_2m + +To implement the result in equation :eq:`inner_products_boundary_conditions_2m` practically, +we expression equation :eq:`inner_products_boundary_conditions_1` in the case of +Neumann boundary conditions as: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T (D^T M_c - B) \, \phi + u^T b} + :label: inner_products_boundary_conditions_1n + +where + + - :math:`\boldsymbol{B} = \boldsymbol{P_{nf}^T} \, diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{P}_{nf}} \, diag ( \boldsymbol{\tilde{c}} ) \boldsymbol{\tilde{P}_{en}}` + - :math:`\boldsymbol{b} = \boldsymbol{P_{nf}^T} \, diag(\boldsymbol{\tilde{a}}) \, \boldsymbol{\tilde{P}_{nf}} \boldsymbol{\tilde{d}}` + +.. _inner_products_boundary_conditions_curl: + +Curl +---- + +Let :math:`\vec{u}` and :math:`\vec{w}` be vector quantities. +If the discrete representation :math:`\boldsymbol{u}` lives on the edges and +the discrete representation :math:`\boldsymbol{w}` lives on the faces, +the inner product is approximated by: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w}) \, dv + \approx \boldsymbol{u^T C^T M_f \, w} - \oint_{\partial \Omega} (\vec{u} \times \vec{w}) \cdot d\vec{a} + :label: inner_products_boundary_conditions_3 + +where + + - :math:`\boldsymbol{C}` is a :ref:`discrete curl operator ` + - :math:`\boldsymbol{M_f}` is the :ref:`basic inner product matrix for vectors on cell faces ` + + +.. figure:: ../../images/boundary_conditions_curl.png + :align: center + :width: 550 + + Figure 3: Quantities used to implement boundary conditions on the curl for an x-face. + + +A discrete approximation to the surface integral in equation +:eq:`inner_products_boundary_conditions_3` which imposes +boundary conditions on :math:`\vec{w}` can be expressed as: + +.. math:: + \oint_{\partial \Omega} (\vec{u} \times \vec{w}) \cdot d\vec{a} + \approx \boldsymbol{u^T} + \begin{bmatrix} \boldsymbol{P_1} \\ \boldsymbol{P_2} \end{bmatrix}^T + \begin{bmatrix} \boldsymbol{\Lambda} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{-\Lambda} \end{bmatrix} + \begin{bmatrix} \boldsymbol{P_1} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_2} \end{bmatrix} + \boldsymbol{\tilde{w}} + :label: inner_products_boundary_conditions_3a + +where + +.. math:: + \boldsymbol{P_1} = + \begin{bmatrix} + 0 & P_{yx} & 0 \\ 0 & 0 & P_{zy} \\ P_{xz} & 0 & 0 + \end{bmatrix} + \textrm{,} \;\; + \boldsymbol{P_2} = + \begin{bmatrix} + 0 & 0 & P_{zx} \\ P_{xy} & 0 & 0 \\ 0 & P_{yz} & 0 + \end{bmatrix} + \textrm{,} \;\; + \boldsymbol{\Lambda} = diag(\boldsymbol{\tilde{a}}) + \;\; \textrm{and}\;\; + \boldsymbol{\tilde{w}} = + \begin{bmatrix} + \boldsymbol{\tilde{w}_{yx}} \\ \boldsymbol{\tilde{w}_{zy}} \\ \boldsymbol{\tilde{w}_{xz}} \\ + \boldsymbol{\tilde{w}_{zx}} \\ \boldsymbol{\tilde{w}_{xy}} \\ \boldsymbol{\tilde{w}_{yz}} + \end{bmatrix} + +such that + + - :math:`\boldsymbol{u}` is the discrete representation of :math:`\vec{u}` on edges + - :math:`P_{ij}` is sparse matrix that projects a quantity from :math:`i` edges to :math:`j` faces for :math:`i,j=x,y,z` + - :math:`\boldsymbol{\tilde{w}_{ij}}` is a vector that stores an approximation of component :math:`i` of :math:`\vec{w}` on :math:`j` edges for :math:`i,j=x,y,z` + - :math:`\boldsymbol{\tilde{a}}` is a vector which stores the dot products between the surface area vectors of the boundary faces :math:`\vec{a}` and the unit direction of :math:`\vec{u} \times \vec{w}`. That is, :math:`\boldsymbol{\tilde{a}} = \pm a` for faces on the boundary and :math:`\boldsymbol{\tilde{a}}=0` for all interior faces + +If we are able to construct matrices :math:`\boldsymbol{P_{ij}}` and :math:`\boldsymbol{\Lambda}`, we must turn our attention +to constructing :math:`\boldsymbol{\tilde{w}}`. + + +Dirichlet +^^^^^^^^^ + +For a 3D mesh, examine a single boundary cell whose surface area vector points in the x-direction +(:ref:`Figure 3 `); i.e. :math:`\vec{a} = a\hat{x}`. +When imposing Dirichlet conditions, we know the value of :math:`\vec{w}` on the boundary. +For boundary face :math:`k` on the aforemention cell, the contribution towards the surface integral is given by: + +.. math:: + \oint_{\partial \Omega_k} (\vec{u} \times \vec{w}) \, d\vec{a} = \int_{\partial \Omega_k} (u_y \tilde{w}_z - u_z \tilde{w}_y) \, da + :label: inner_products_boundary_conditions_3b + +where :math:`\tilde{w}_y` and :math:`\tilde{w}_z` are component values of :math:`\vec{w}` at the same edge +locations as :math:`u_z` and :math:`u_y`, respectively. Thus for boundary face :math:`k` +with surface area :math:`a_k`, the approximate contribution towards the surface integral +denoted by equation :eq:`inner_products_boundary_conditions_3b` is: + +.. math:: + \oint_{\partial \Omega_k} (\vec{u} \times \vec{w}) \cdot d\vec{a} = \frac{a_k}{4} \! & \bigg [ \! + \bigg ( \! u_y \big ( i, j\! +\! \tfrac{1}{2}, k \big ) \! +\! u_y \big ( i, j\! +\! \tfrac{1}{2}, k\! +\! 1 \big ) \! \bigg ) \! + \bigg ( \! \tilde{w}_z \big ( i, j\! +\! \tfrac{1}{2}, k \big ) \! +\! \tilde{w}_z \big ( i, j\! +\! \tfrac{1}{2}, k\! +\! 1 \big ) \! \bigg ) \\ + -& \bigg ( \! u_z \big ( i, j, k\! +\! \tfrac{1}{2} \big ) \! +\! u_z \big ( i, j\! +\! 1, k\! +\! \tfrac{1}{2} \big ) \! \bigg ) \! + \bigg ( \! \tilde{w}_y \big ( i, j, k\! +\! \tfrac{1}{2} \big ) \! +\! \tilde{w}_y \big ( i, j\! +\! 1, k\! +\! \tfrac{1}{2} \big ) \! \bigg ) \! \Bigg ] + :label: inner_products_boundary_conditions_3c + +Assuming we have constructed matrices :math:`\boldsymbol{P_{ij}}` and :math:`\boldsymbol{\Lambda}`, +we can construct a vector :math:`\boldsymbol{\tilde{w}}` such that values :math:`\vec{w}` on boundary edges are defined +by the boundary condition and :math:`\vec{w}` on interior edges are set to zero. + + +Neumann +^^^^^^^ + +According to equation :eq:`inner_products_boundary_conditions_3a`, we need to construct a large edge vector +:math:`\boldsymbol{\tilde{w}}` when the derivative normal to the surface is known. +When imposing the Neuwmann condition, we know the value of :math:`\frac{\partial \vec{w}}{\partial n}` on the boundary. +For a 3D mesh, examine a single boundary cell whose surface area vector points in the x-direction +(:ref:`Figure 3 `); i.e. :math:`\vec{a} = a\hat{x}`. + +Once again, we define a Taylor expansion to approximated the values of functions on the boundary. +For :math:`w_z`, we define a Taylor expansion at position :math:`(i, j \! + \! \tfrac{1}{2},k)`. +Solving the approximation :math:`\tilde{w}_z` on the boundary we obtain: + +.. math:: + \tilde{w}_z (i, j \! + \! \tfrac{1}{2},k) = w_z (i \! - \! \tfrac{1}{2}, j \! + \! \tfrac{1}{2},k) - \frac{\partial w_z}{\partial n} \Delta s_x + :label: inner_products_boundary_conditions_3d + +where :math:`\Delta s_x` is the path from position :math:`(i, j \! + \! \tfrac{1}{2},k)` +to position :math:`(i \! - \! \tfrac{1}{2}, j \! + \! \tfrac{1}{2},k)`. +Similarly for :math:`w_y`: + +.. math:: + \tilde{w}_y (i, j, k \! + \! \tfrac{1}{2}) = w_y (i \! - \! \tfrac{1}{2}, j, k \! + \! \tfrac{1}{2}) - \frac{\partial w_y}{\partial n} \Delta s_x + :label: inner_products_boundary_conditions_3e + +The same approach can be used to approximate the value of all required components on all boundary edges. +Much like we did for the gradient and divergence operators, we can define the vector :math:`\boldsymbol{\tilde{w}}` in terms of a linear operation: + +.. math:: + \boldsymbol{\tilde{w}} = \boldsymbol{\tilde{P} w + \tilde{b}} + :label: inner_products_boundary_conditions_3f + +where + + - :math:`\boldsymbol{\tilde{P}}` maps the entries of :math:`\boldsymbol{w}` to the edges where they are needed + - :math:`\boldsymbol{\tilde{b}}` is :math:`-\frac{\partial w_k}{\partial n}` on boundary faces and 0 for interior faces + +To implement the result in equation :eq:`inner_products_boundary_conditions_3f` practically, +we express equation :eq:`inner_products_boundary_conditions_3` in the case of +Neumann boundary conditions as: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w}) \, dv + \approx \boldsymbol{u^T (C^T M_f - B ) \, w - u^T b} + :label: inner_products_boundary_conditions_3g + +where + +.. math:: + \boldsymbol{B} = \begin{bmatrix} \boldsymbol{P_1} \\ \boldsymbol{P_2} \end{bmatrix}^T + \begin{bmatrix} \boldsymbol{\Lambda} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{-\Lambda} \end{bmatrix} + \begin{bmatrix} \boldsymbol{P_1} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_2} \end{bmatrix} + \boldsymbol{\tilde{P}} + + +and + +.. math:: + \boldsymbol{b} = \begin{bmatrix} \boldsymbol{P_1} \\ \boldsymbol{P_2} \end{bmatrix}^T + \begin{bmatrix} \boldsymbol{\Lambda} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{-\Lambda} \end{bmatrix} + \begin{bmatrix} \boldsymbol{P_1} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_2} \end{bmatrix} \boldsymbol{\tilde{b}} + + +Mixed +^^^^^ + +Where :math:`\alpha`, :math:`\beta` and :math:`\gamma` are constants, +the boundary conditions are defined by the following ordinary differential equation: + +.. math:: + \alpha \phi + \beta \frac{\partial \phi}{\partial n} = \gamma + :label: inner_products_boundary_conditions_3h + +In this case, the Taylor expansions described in expressios :eq:`inner_products_boundary_conditions_3d` +and :eq:`inner_products_boundary_conditions_3e` become: + +.. math:: + \tilde{w}_z (i, j \! + \! \tfrac{1}{2},k) = + \Bigg ( \frac{\beta}{\beta - \alpha \Delta s_x} \Bigg ) w_z (i \! - \! \tfrac{1}{2}, j \! + \! \tfrac{1}{2},k) + - \Bigg ( \frac{\gamma \Delta s_x}{\beta - \alpha \Delta s_x} \Bigg ) + +and + +.. math:: + \tilde{w}_y (i, j,k \! + \! \tfrac{1}{2}) = + \Bigg ( \frac{\beta}{\beta - \alpha \Delta s_x} \Bigg ) w_y (i \! - \! \tfrac{1}{2}, j,k \! + \! \tfrac{1}{2}) + - \Bigg ( \frac{\gamma \Delta s_x}{\beta - \alpha \Delta s_x} \Bigg ) + +where the derivatives were approximated to first order using backward difference. +Once again, the Taylor expansion can be used to approximate all required components of :math:`\vec{w}` +at all necessary boundary edges. + +Similarly to how we defined :math:`\boldsymbol{\tilde{w}}` in :eq:`inner_products_boundary_conditions_3f`, we obtain: + +.. math:: + \boldsymbol{\tilde{w}} = diag(\boldsymbol{\tilde{c}}) \boldsymbol{\tilde{P} w + \tilde{d}} + :label: inner_products_boundary_conditions_3i + +where + +.. math:: + \boldsymbol{\tilde{c}} = \begin{cases} \frac{\beta}{\beta - \alpha \Delta s_k} \;\;\textrm{on boundary edges}\\ 0 \;\; \textrm{on all other edges} \end{cases} + +and + +.. math:: + \boldsymbol{\tilde{d}} = \begin{cases} - \frac{\gamma \Delta s_k}{\beta - \alpha \Delta s_k} \;\;\textrm{on boundary edges}\\ 0 \;\; \textrm{on all other other edges} \end{cases} + +To implement the result in equation :eq:`inner_products_boundary_conditions_3i` practically, +we express equation :eq:`inner_products_boundary_conditions_3` in the case of +Neumann boundary conditions as: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w}) \, dv + \approx \boldsymbol{u^T (C^T M_f - B ) \, w - u^T b} + :label: inner_products_boundary_conditions_3g + +where + +.. math:: + \boldsymbol{B} = \begin{bmatrix} \boldsymbol{P_1} \\ \boldsymbol{P_2} \end{bmatrix}^T + \begin{bmatrix} \boldsymbol{\Lambda} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{-\Lambda} \end{bmatrix} + \begin{bmatrix} \boldsymbol{P_1} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_2} \end{bmatrix} + diag(\boldsymbol{\tilde{c}}) \boldsymbol{\tilde{P}} + + +and + +.. math:: + \boldsymbol{b} = \begin{bmatrix} \boldsymbol{P_1} \\ \boldsymbol{P_2} \end{bmatrix}^T + \begin{bmatrix} \boldsymbol{\Lambda} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{-\Lambda} \end{bmatrix} + \begin{bmatrix} \boldsymbol{P_1} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_2} \end{bmatrix} \boldsymbol{\tilde{d}} + + + + + diff --git a/docs/content/theory/inner_products_differential.rst b/docs/content/theory/inner_products_differential.rst new file mode 100644 index 000000000..ebb06af33 --- /dev/null +++ b/docs/content/theory/inner_products_differential.rst @@ -0,0 +1,297 @@ +.. _inner_products_differential: + +Differential Operators +********************** + +Introduction +------------ + +For practical applications of the finite volume method, +we may need to take the inner product of expressions containing differential operators. +These operators include the: + + - the gradient (:math:`\nabla \phi` ) + - the divergence (:math:`\nabla \cdot \vec{w}` ) + - the curl (:math:`\nabla \times \vec{w}` ) + +For scalar quantities :math:`\psi` and :math:`\phi` and for vector quantities :math:`\vec{u}` and :math:`\vec{w}` +we are interested in approximating the following inner products: + +.. math:: + \begin{align} + (\vec{u}, \nabla \phi ) &= \int_\Omega \vec{u} \cdot \nabla \phi \, dv\\ + (\psi, \nabla \cdot \vec{w} ) &= \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \\ + (\vec{u}, \nabla \times \vec{w} ) &= \int_\Omega \vec{u} \cdot (\nabla \times \vec{w} ) \, dv + \end{align} + +**Tutorial:** To construct differential operators and/or approximate inner products of this type, see the :ref:`tutorial on inner products with differential operators ` + + +.. _inner_products_differential_gradient: + +Gradient +^^^^^^^^ + +For the inner product between a vector :math:`\vec{u}` and the gradient of a scalar :math:`\phi`, +there are two options for where the variables should live. For :math:`\boldsymbol{\phi}` defined on the nodes +and :math:`\boldsymbol{u}` defined on cell edges: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx \boldsymbol{u^T M_e G \, \phi} + +And for :math:`\boldsymbol{\phi}` defined at cell centers and :math:`\boldsymbol{u}` defined on cell faces: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T D^T M_c \, \phi} + B.\! C. + +where + + - :math:`\boldsymbol{G}` is a :ref:`discrete gradient operator ` + - :math:`\boldsymbol{D}` is a :ref:`discrete divergence operator ` + - :math:`\boldsymbol{M_c}` is the :ref:`basic inner product matrix for vectors at cell centers ` + - :math:`\boldsymbol{M_e}` is the :ref:`basic inner product matrix for vectors at edges ` + - :math:`B.\! C.` represents an additional term that must be constructed to impose boundary conditions correctly on :math:`\phi`. It is zero when :math:`\phi = 0` on the boundary. + + +.. _inner_products_differential_divergence: + +Divergence +^^^^^^^^^^ + +For the inner product between a scalar (:math:`\psi`) and the divergence of a vector (:math:`\vec{w}`), +there are two options for where the variables should live. For :math:`\boldsymbol{\psi}` defined at cell centers +and :math:`\boldsymbol{w}` defined on cell faces: + +.. math:: + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx \boldsymbol{\psi^T M_c D \, w} + +And for :math:`\boldsymbol{\psi}` defined on the nodes and :math:`\boldsymbol{w}` defined on cell edges: + +.. math:: + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx - \boldsymbol{\psi^T G^T M_e \, w } + B.\! C. + +where + + - :math:`\boldsymbol{G}` is a :ref:`discrete gradient operator ` + - :math:`\boldsymbol{D}` is a :ref:`discrete divergence operator ` + - :math:`\boldsymbol{M_c}` is the :ref:`basic inner product matrix for vectors at cell centers ` + - :math:`\boldsymbol{M_e}` is the :ref:`basic inner product matrix for vectors at edges ` + - :math:`B.\! C.` represents an additional term that must be constructed to impose boundary conditions correctly on :math:`\vec{w}`. It is zero when :math:`\hat{n} \cdot \vec{w} = 0` on the boundary. + + +.. _inner_products_differential_curl: + +Curl +^^^^ + +For the inner product between a vector (:math:`\vec{u}`) and the curl of another vector (:math:`\vec{w}`), +there are two options for where the variables should live. For :math:`\boldsymbol{u}` defined on the faces +and :math:`\boldsymbol{w}` defined on cell edges: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w} ) \, dv \approx \boldsymbol{u^T M_f C \, w} + +And for :math:`\boldsymbol{u}` defined on the edges and :math:`\boldsymbol{w}` defined on cell faces: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w} ) \, dv \approx \boldsymbol{u^T C^T \! M_f \, w } + B.\! C. + +where + + - :math:`\boldsymbol{C}` is a :ref:`discrete curl operator ` + - :math:`\boldsymbol{M_f}` is the :ref:`basic inner product matrix for vectors on cell faces ` + - :math:`B.\! C.` represents an additional term that must be constructed to impose boundary conditions correctly on :math:`\vec{w}`. It is zero when :math:`\hat{n} \times \vec{w} = 0` on the boundary. + +.. _inner_products_differential_gradient_full: + +Vector and the Gradient of a Scalar +----------------------------------- + +Let :math:`\phi` be a scalar quantity and let :math:`\vec{u}` be a vector quantity. +We are interested in approximating the following inner product: + +.. math:: + (\vec{u}, \nabla \phi ) = \int_\Omega \vec{u} \cdot \nabla \phi \, dv + :label: inner_products_differential_gradient + +Inner Product on Edges +^^^^^^^^^^^^^^^^^^^^^^ + +Here, the discrete representation :math:`\boldsymbol{\phi}` lives on the nodes and +the discrete representation :math:`\boldsymbol{u}` lives on the edges. +Since the :ref:`discrete gradient operator ` maps +a discrete scalar quantity from nodes to edges, we can approximate the inner product +between two discrete quantities living on the edges. Thus: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx \boldsymbol{u^T M_e G \, \phi} + +where + + - :math:`\boldsymbol{G}` is the :ref:`discrete gradient operator ` + - :math:`\boldsymbol{M_e}` is the :ref:`basic inner product matrix for vectors at edges ` + + +Inner Product on Faces +^^^^^^^^^^^^^^^^^^^^^^ + +Here, the discrete representation :math:`\boldsymbol{\phi}` lives at cell centers and +the discrete representation :math:`\boldsymbol{u}` lives on the faces. +In this case we cannot simply use a discrete gradient operator, as a mapping from cell centers +to faces would require knowledge of the scalar at locations outside the mesh. + +To evaluate the inner product we use the identity +:math:`\vec{u} \cdot \nabla \phi = \nabla \cdot \phi\vec{u} - \phi \nabla \cdot \vec{u}` +and apply the divergence theorem to equation :eq:`inner_products_differential_gradient`: + +.. math:: + \begin{align} + \int_\Omega \vec{u} \cdot \nabla \phi \, dv &= - \int_\Omega \phi \nabla \cdot \vec{u} \, dv + \int_\Omega \nabla \cdot \phi\vec{u} \, dv \\ + &= - \int_\Omega \phi \nabla \cdot \vec{u} \, dv + \oint_{\partial \Omega} \hat{n} \cdot \phi\vec{u} \, da \\ + &= - \int_\Omega \phi \nabla \cdot \vec{u} \, dv + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da + \end{align} + :label: inner_products_differential_gradient_centers + +Where boundary conditions are implemented in the surface integral. The approximate to the inner product is given by: + +.. math:: + \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx - \boldsymbol{u^T D^T M_c \, \phi} + B.\! C. + +where + + - :math:`\boldsymbol{D}` is the :ref:`discrete divergence operator ` + - :math:`\boldsymbol{M_c}` is the :ref:`basic inner product matrix for vectors at cell centers ` + - :math:`B.\! C.` represents an additional term that must be constructed to impose boundary conditions correctly on :math:`\phi` + +When formulating the approximation to the inner product in this way, the natural boundary condition is a Dirichlet condition such that :math:`\phi = 0` on the boundary. +In this case, the added boundary condition term is zero. + +.. _inner_products_differential_divergence_full: + +Scalar and the Divergence of a Vector +------------------------------------- + +Let :math:`\psi` be a scalar quantity and let :math:`\vec{w}` be a vector quantity. +We are interested in approximating the following inner product: + +.. math:: + (\psi, \nabla \cdot \vec{w} ) = \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \\ + :label: inner_products_differential_divergence + + +Inner Product on Faces +^^^^^^^^^^^^^^^^^^^^^^ + +Here, the discrete representation :math:`\boldsymbol{\psi}` lives at cell centers and +the discrete representation :math:`\boldsymbol{w}` lives on the faces. +Since the :ref:`discrete divergence operator ` maps +a discrete vector quantity from faces to cell centers, we can approximate the inner product +between two discrete quantities living at the centers. Thus: + +.. math:: + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx \boldsymbol{\psi^T M_c D \, w} + +where + + - :math:`\boldsymbol{D}` is the :ref:`discrete divergence operator ` + - :math:`\boldsymbol{M_c}` is the :ref:`basic inner product matrix for vectors at cell centers ` + + +Inner Product on Edges +^^^^^^^^^^^^^^^^^^^^^^ + +Here, the discrete representation :math:`\boldsymbol{\psi}` lives on the nodes and +the discrete representation :math:`\boldsymbol{w}` lives on the edges. +We cannot simply use a discrete divergence operator, as a mapping from edges +to nodes would require knowledge of :math:`\vec{w}` at locations outside the mesh. + +To evaluate the inner product we use the identity +:math:`\psi \nabla \cdot \vec{w} = \nabla \cdot \psi\vec{w} - \vec{w} \cdot \nabla \psi` +and apply the divergence theorem to equation :eq:`inner_products_differential_gradient`: + +.. math:: + \begin{align} + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv &= - \int_\Omega \vec{w} \cdot \nabla \psi \, dv + \int_\Omega \nabla \cdot \psi\vec{w} \, dv \\ + &= - \int_\Omega \vec{w} \cdot \nabla \psi \, dv + \oint_{\partial \Omega} \hat{n} \cdot \psi\vec{w} \, da \\ + &= - \int_\Omega \vec{w} \cdot \nabla \psi \, dv + \oint_{\partial \Omega} \psi (\hat{n} \cdot \vec{w}) \, da + \end{align} + :label: inner_products_differential_divergence_edges + +Where boundary conditions are implemented in the surface integral. The approximate to the inner product is given by: + +.. math:: + \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx - \boldsymbol{\psi^T G^T M_e \, w} + B.\! C. + +where + + - :math:`\boldsymbol{G}` is the :ref:`discrete gradient operator ` + - :math:`\boldsymbol{M_e}` is the :ref:`basic inner product matrix for vectors at the edges ` + - :math:`B.\! C.` represents an additional term that must be constructed to impose boundary conditions correctly on :math:`\vec{w}` + +When formulating the approximation to the inner product in this way, the natural boundary condition is for :math:`\hat{n} \cdot \vec{w} = 0` on the boundary. +In this case, the added boundary condition term is zero. + +.. _inner_products_differential_curl_full: + +Vector and the Curl of a Vector +------------------------------- + +Let :math:`\vec{u}` and :math:`\vec{w}` be vector quantities. +We are interested in approximating the following inner product: + +.. math:: + (\vec{u}, \nabla \times \vec{w} ) = \int_\Omega \vec{u} \cdot (\nabla \times \vec{w} ) \, dv + :label: inner_products_differential_curl + + +Inner Product at Faces +^^^^^^^^^^^^^^^^^^^^^^ + +Here, the discrete representation :math:`\boldsymbol{u}` lives on the faces and +the discrete representation :math:`\boldsymbol{w}` lives on the edges. +Since the :ref:`discrete curl operator ` maps +a discrete vector quantity from edges to faces, we can approximate the inner product +between two discrete quantities living on the faces. Thus: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w}) \, dv \approx \boldsymbol{u^T M_f C w} + +where + + - :math:`\boldsymbol{C}` is the :ref:`discrete curl operator ` + - :math:`\boldsymbol{M_f}` is the :ref:`basic inner product matrix for vectors on faces ` + + +Inner Product at Edges +^^^^^^^^^^^^^^^^^^^^^^ + +Here, the discrete representation :math:`\boldsymbol{u}` lives on the edges and +the discrete representation :math:`\boldsymbol{w}` lives on the faces. +We cannot simply use the discrete curl operator, as a mapping from faces +to edges would require knowledge of :math:`\boldsymbol{w}` at locations outside the mesh. + +To evaluate the inner product we use the identity +:math:`\vec{u} \cdot (\nabla \times \vec{w}) = \vec{w} \cdot (\nabla \times \vec{u}) - \nabla \cdot (\vec{u} \times \vec{w})` +and apply the divergence theorem to equation :eq:`inner_products_differential_curl`: + +.. math:: + \begin{align} + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w}) \, dv &= \int_\Omega \vec{w} \cdot (\nabla \times \vec{u}) \, dv - \int_\Omega \nabla \cdot (\vec{u} \times \vec{w}) \, dv \\ + &= \int_\Omega \vec{w} \cdot (\nabla \times \vec{u}) \, dv - \oint_{\partial \Omega} \hat{n} \cdot (\vec{u} \times \vec{w}) \, da \\ + &= \int_\Omega \vec{w} \cdot (\nabla \times \vec{u}) \, dv - \oint_{\partial \Omega} (\vec{u} \times \vec{w}) \cdot d\vec{a} + \end{align} + :label: inner_products_differential_curl_edges + +Where boundary conditions are implemented in the surface integral. The approximate to the inner product is given by: + +.. math:: + \int_\Omega \vec{u} \cdot (\nabla \times \vec{w}) \, dv \approx \boldsymbol{u^T C^T M_f \, w} + B.\! C. + +where + + - :math:`\boldsymbol{C}` is the :ref:`discrete curl operator ` + - :math:`\boldsymbol{M_f}` is the :ref:`basic inner product matrix for vectors on faces ` + - :math:`B.\! C.` represents an additional term that must be constructed to impose boundary conditions correctly on :math:`\vec{w}` + +When formulating the approximation to the inner product in this way, the natural boundary condition is for :math:`\hat{n} \times \vec{w} = 0` on the boundary. +In this case, the added boundary condition term is zero. diff --git a/docs/content/theory/inner_products_index.rst b/docs/content/theory/inner_products_index.rst new file mode 100644 index 000000000..ff24c1242 --- /dev/null +++ b/docs/content/theory/inner_products_index.rst @@ -0,0 +1,41 @@ +.. _inner_products_index: + +Inner Products +************** + +Inner products provide the building blocks for discretizing and solving PDEs with the +finite volume method (:cite:`haber2014,HymanShashkov1999`). For scalar quantities :math:`\psi` and :math:`\phi`, the inner +product is given by: + +.. math:: + (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv + +And for two vector quantities :math:`\vec{u}` and :math:`\vec{v}`, the inner product is +given by: + +.. math:: + (\vec{u}, \vec{v}) = \int_\Omega \vec{u} \cdot \vec{v} \, dv + +When implementing the finite volume method, we construct discrete approximations to inner products. +The approximations to inner products are combined and simplified to form a linear system in terms +of an unknown variable, then solved. The approximation to each inner product depends on the quantities +and differential operators present in the inner product. Here, we demonstrate how to formulate the +discrete approximation for several classes of inner products. + +**Contents:** + +.. toctree:: + :maxdepth: 1 + + inner_products_basic + inner_products_isotropic + inner_products_anisotropic + inner_products_differential + inner_products_boundary_conditions + +**Tutorials:** + +- :ref:`Basic Inner Products ` +- :ref:`Inner Products with Constitutive Relationships ` +- :ref:`Inner Products with Differential Operators ` +- :ref:`Advanced Inner Product Examples ` \ No newline at end of file diff --git a/docs/content/theory/inner_products_isotropic.rst b/docs/content/theory/inner_products_isotropic.rst new file mode 100644 index 000000000..b6db9d624 --- /dev/null +++ b/docs/content/theory/inner_products_isotropic.rst @@ -0,0 +1,238 @@ +.. _inner_products_isotropic: + +Isotropic Constitutive Relationships +************************************ + +Summary +------- + +A constitutive relationship quantifies the response of a material to an external stimulus. +Examples include Ohm's law and Hooke's law. For practical applications of the finite volume method, +we may need to take the inner product of expressions containing constitutive relationships. + +Let :math:`\vec{v}` and :math:`\vec{w}` be two physically related quantities. +If their relationship is isotropic (defined by a constant :math:`\sigma`), +then the constitutive relation is given by: + +.. math:: + \vec{v} = \sigma \vec{w} + :label: inner_product_isotropic + +Here we show that for isotropic constitutive relationships, the inner +product between a vector :math:`\vec{u}` and the right-hand side of +equation :eq:`inner_product_isotropic` is approximated by: + +.. math:: + (\vec{u}, \sigma \vec{w} ) = \int_\Omega \vec{u} \cdot \sigma \vec{w} \, dv \approx \boldsymbol{u^T M w} + +where :math:`\boldsymbol{M}` represents an *inner-product matrix*, and vectors +:math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are discrete variables that live +on the mesh. It is important to note a few things about the +inner-product matrix in this case: + + 1. It depends on the dimensions and discretization of the mesh + 2. It depends on where the discrete variables live; e.g. edges, faces + 3. It depends on the spacial variation of the material property :math:`\sigma` + +For this class of inner products, the corresponding inner product matricies for +discrete quantities living on various parts of the mesh are given by: + +.. math:: + \textrm{Vectors on faces:} \; \boldsymbol{M_{\sigma f}} &= \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} \boldsymbol{\big ( e_k \otimes (v \odot \sigma ) \big )} \boldsymbol{P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_{\sigma e}} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} \boldsymbol{\big ( e_k \otimes (v \odot \sigma) \big )} \boldsymbol{P_e} + +where + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from faces and edges to cell centers, respectively + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{\sigma}` is a vector containing the physical property values for the cells + + +**Tutorial:** To construct the inner product matrix and/or approximate inner products of this type, see the :ref:`tutorial on inner products with constitutive relationships ` + +.. _inner_products_isotropic_faces: + +Vectors on Cell Faces +--------------------- + +We want to approximate the inner product between a vector quantity :math:`\vec{u}` and the product of +:math:`\sigma` and :math:`\vec{w}`. Here, we discretize such that :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live on cess faces. Our goal is to construct the inner product matrix :math:`\boldsymbol{M}` in the expression below: + +.. math:: + (\vec{u}, \sigma \vec{w}) = \int_\Omega \vec{u} \cdot \sigma \vec{w} \, dv \approx \boldsymbol{u^T M \, w} + :label: inner_product_isotropic_faces + +We must respect the dot product. For vectors defined on cell faces, we discretize such that the +x-component of the vectors live on the x-faces, the y-component lives y-faces and the z-component +lives on the z-faces. For a single cell, this is illustrated in 2D and 3D below. By decomposing the +domain into a set of finite cells, we assume the physical properties are spacial invariant within each cell. + +.. figure:: ../../images/face_discretization.png + :align: center + :width: 600 + +As we can see there are 2 faces for each component. Therefore, we need to project each component of the +vector from its faces to the cell centers and take their averages separately. +For a single cell with volume :math:`v_i` and material property value :math:`\sigma_i`, +the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i \sigma_i}{4} \Big ( u_x^{(1)} + u_x^{(2)} \Big ) \Big ( w_x^{(1)} + w_x^{(2)} \Big ) \\ + & + \frac{v_i \sigma_i}{4} \Big ( u_y^{(1)} + u_y^{(2)} \Big ) \Big ( w_y^{(1)} + w_y^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i \sigma_i}{4} \Big ( u_x^{(1)} + u_x^{(2)} \Big ) \Big ( w_x^{(1)} + w_x^{(2)} \Big ) \\ + & + \frac{v_i \sigma_i}{4} \Big ( u_y^{(1)} + u_y^{(2)} \Big ) \Big ( w_y^{(1)} + w_y^{(2)} \Big ) \\ + & + \frac{v_i \sigma_i}{4} \Big ( u_z^{(1)} + u_z^{(2)} \Big ) \Big ( w_z^{(1)} + w_z^{(2)} \Big ) + \end{align} + :label: inner_product_isotropic_faces_1 + +where superscripts :math:`(1)` and :math:`(2)` denote face 1 and face 2, respectively. +Using the contribution for each cell described in expression :eq:`inner_product_isotropic_faces_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_isotropic_faces`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_f}` which projects quantities on the x, y and z faces separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell faces as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{e_x} \\ \boldsymbol{e_y} \\ \boldsymbol{e_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \sigma \vec{w}) = \int_\Omega \vec{u} \cdot \sigma \vec{w} \, dv \approx \boldsymbol{\boldsymbol{u} M_{\sigma f}} \, \boldsymbol{w} + +where the mass matrix has the form: + +.. math:: + \boldsymbol{M_{\sigma f}} = \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} \boldsymbol{\big ( e_k \otimes (v \odot \sigma ) \big )} \boldsymbol{P_f} + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_f}` is a projection matrix that maps from faces to cell centers + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{\sigma}` is a vector containing the physical property values for the cells + +.. _inner_products_isotropic_edges: + +Vectors on Cell Edges +--------------------- + +We want to approximate the inner product between a vector quantity :math:`\vec{u}` and the product of +:math:`\sigma` and :math:`\vec{w}`. Here, we discretize such that :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` are defined +to live at cell edges. Our goal is to construct the inner product matrix :math:`\boldsymbol{M}` in the expression below: + +.. math:: + (\vec{u}, \sigma \vec{w}) = \int_\Omega \vec{u} \cdot \sigma \vec{w} \, dv \approx \boldsymbol{u^T M \, w} + :label: inner_product_isotropic_edges + +We must respect the dot product. For vectors defined on cell edges, we discretize such that the +x-component of the vectors live on the x-edges, the y-component lives y-edges and the z-component +lives on the z-edges. This is illustrated in 2D and 3D below. By decomposing the +domain into a set of finite cells, we assume the material properties are spacial invariant within each cell. + +.. figure:: ../../images/edge_discretization.png + :align: center + :width: 600 + +As we can see there are 2 edges for each component in 2D and 4 edges for each component in 3D. +Therefore, we need to project each component of the +vector from its edges to the cell centers and take their averages separately. For a single cell with volume :math:`v_i` +and material property value :math:`\sigma_i`, the contribution towards the inner product is: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i \sigma_i}{4} \Big ( u_x^{(1)} + u_x^{(2)} \Big ) \Big ( w_x^{(1)} + w_x^{(2)} \Big ) \\ + & + \frac{v_i \sigma_i}{4} \Big ( u_y^{(1)} + u_y^{(2)} \Big ) \Big ( w_y^{(1)} + w_y^{(2)} \Big ) \\ + & \\ + \mathbf{In \; 3D:} \; \int_{\Omega_i} \vec{u} \cdot \vec{w} \, dv \approx & \;\; \frac{v_i \sigma_i}{16} \Bigg ( \sum_{n=1}^4 u_x^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^4 w_x^{(n)} \Bigg ) \\ + & + \frac{v_i \sigma_i}{16} \Bigg ( \sum_{n=1}^4 u_y^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^4 w_y^{(n)} \Bigg ) \\ + & + \frac{v_i \sigma_i}{16} \Bigg ( \sum_{n=1}^4 u_z^{(n)} \Bigg ) \Bigg ( \sum_{n=1}^4 w_z^{(n)} \Bigg ) + \end{align} + :label: inner_product_isotropic_edges_1 + +where the superscript :math:`(n)` denotes a particular edge. +Using the contribution for each cell described in expression :eq:`inner_product_isotropic_edges_1`, +we want to approximate the inner product in the form described by +equation :eq:`inner_product_isotropic_edges`. To accomlish this, we construct a sparse matrix +:math:`\boldsymbol{P_e}` which projects quantities on the x, y and z edges separately to the +the cell centers. + +For discretize vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` whose x, y (and z) components +are organized on cell edges as follows: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_y} \\ \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; + \boldsymbol{w} = \begin{bmatrix} \boldsymbol{e_x} \\ \boldsymbol{e_y} \\ \boldsymbol{e_y} \\ \end{bmatrix} + +the approximation to the inner product is given by: + +.. math:: + (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv \approx \boldsymbol{\boldsymbol{u} M_e \, \boldsymbol{w}} + +where the mass matrix for face quantities has the form: + +.. math:: + \boldsymbol{M_{\sigma e}} = \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} \boldsymbol{\big ( e_k \otimes (v \odot \sigma) \big )} \boldsymbol{P_e} + +and + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_e}` is a projection matrix that maps from edges to cell centers + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{\sigma}` is a vector containing the physical property values for the cells + +.. _inner_products_isotropic_reciprocal: + + +Reciprocal Properties +--------------------- + +Let :math:`\vec{v}` and :math:`\vec{w}` be two physically related quantities. +If their relationship is isotropic and defined by the reciprocal of a physical property (defined by a constant :math:`\rho`), +then the constitutive relation is given by: + +.. math:: + \vec{v} = \rho^{-1} \vec{w} + :label: inner_product_isotropic_reciprocal + +Because the relationship between :math:`\vec{v}` and :math:`\vec{w}` is a constant, +the derivation of the inner-product matrix at edges and faces is effectively the same. +For this case, the corresponding inner product matricies for +discrete quantities living on various parts of the mesh are given by: + +.. math:: + \textrm{Vectors on faces:} \; \boldsymbol{M_{\rho f}} &= \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} \boldsymbol{\big ( e_k \otimes (v \odot \rho^{-1} ) \big )} \boldsymbol{P_f} \\ + \textrm{Vectors on edges:} \; \boldsymbol{M_{\rho e}} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} \boldsymbol{\big ( e_k \otimes (v \odot \rho^{-1}) \big )} \boldsymbol{P_e} + +where + + - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) + - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` + - :math:`\odot` is the Hadamard product + - :math:`\otimes` is the kronecker product + - :math:`\boldsymbol{P_f}` and :math:`\boldsymbol{P_e}` are projection matricies that map quantities from faces and edges to cell centers, respectively + - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells + - :math:`\boldsymbol{\rho^{-1}}` is a vector containing the reciprocal of :math:`\rho` for all cells + + +**Tutorial:** To construct the inner product matrix and/or approximate inner products of this type, see the :ref:`tutorial on inner products with constitutive relationships ` + diff --git a/docs/content/theory/meshes_index.rst b/docs/content/theory/meshes_index.rst new file mode 100644 index 000000000..24b8088d7 --- /dev/null +++ b/docs/content/theory/meshes_index.rst @@ -0,0 +1,62 @@ +.. _meshes_index: + +Meshes +****** + +A mesh is a numerical grid on which discrete approximations of continuous functions and variables live. +Meshes can be created to discretize and solve 1D, 2D or 3D problems. +For a given system of partial differential equations, the mesh is a discrete representation of the domain and its boundaries. +Here, we demonstrate mesh types supported by the *discretize* package, and where discrete quantities live. + +**Contents:** + + - :ref:`Mesh Types` + - :ref:`Where Quantities Live ` + +**Tutorials:** + + - :ref:`Mesh Types Overview ` + - :ref:`Tensor Meshes ` + - :ref:`Cylindrical Meshes ` + - :ref:`Tree Meshes ` + + +.. _meshes_index_types: + +Mesh Types +========== + +Mesh types supported by the *discretize* package include: + + - **Tensor Meshes:** A mesh where the grid locations are organized according to tensor products + - **Tree Meshes:** A mesh where the dimensions of cells are :math:`2^n` larger than the dimension of the smallest cell size + - **Curvilinear Meshes:** A tensor mesh where the axes are curvilinear + - **Cylindrical Meshes:** A pseudo-2D mesh for solving 3D problems with perfect symmetry in the radial direction + +.. figure:: ../../images/mesh_types.png + :align: center + :width: 700 + + Examples of different mesh types supported by the *discretize* package. + +.. _meshes_index_quantities: + +Where Quantities Live +===================== + +In *discretize*, we use a staggered mimetic finite volume approach :cite:`haber2014,HymanShashkov1999`. +This approach requires the definitions of variables at either cell-centers, nodes, faces, or edges. +Below, we illustrate the valid locations for discrete quantities for a single cell where: + + - **Center:** the location at the center of each cell. + - **Nodes:** locations of intersection between grid lines defining the mesh. + - **X, Y and Z edges:** edges whose tangent lines are parallel to the X, Y and Z axes, respectively. + - **X, Y and Z faces:** faces which are normal to the orientation of the X, Y and Z axes, respectively. + + +.. figure:: ../../images/cell_locations.png + :align: center + :width: 700 + + Locations of centers, nodes, faces and edges for 2D cells (left) and 3D cells (right). + diff --git a/docs/content/theory/operators_averaging.rst b/docs/content/theory/operators_averaging.rst new file mode 100644 index 000000000..4fec57830 --- /dev/null +++ b/docs/content/theory/operators_averaging.rst @@ -0,0 +1,434 @@ +.. _operators_averaging: + +Averaging +********* + +Here, we provide the background theory for how discrete averaging matricies are formed. +Averaging matrices are required when quantities that live on different +parts of the mesh need to be added, subtracted, multiplied or divided. +Averaging matrices are built using the same principles that were discussed when forming :ref:`interpolation matrices `; +except the locations of the original quantity and the interpolated quantity are organized on a structured grid. + +Where :math:`\boldsymbol{u}` is a discrete representation of a vector living somewhere on the mesh (nodes, edges, faces), +and :math:`\bar{\boldsymbol{u}}` is the vector containing the averages mapped to another part of the mesh, +we look to constructs an averaging matrix :math:`\boldsymbol{A}` such that: + +.. math:: + \bar{\boldsymbol{u}} = A \, \boldsymbol{u} + :label: operators_averaging_general + +**Tutorial:** :ref:`tutorial for constructing and applying averaging operators ` + + +Averaging Matrices in 1D +======================== + +Nodes to Cell Centers +^^^^^^^^^^^^^^^^^^^^^ + +Let us define a 1D mesh where cell center and node locations are defined according to the figure below. +The widths of the cells are given by :math:`\Delta x_i`. + +.. figure:: ../../images/averaging_1d.png + :align: center + :width: 600 + + A 1D tensor mesh denoting the node and cell center locations. + +If :math:`u(x)` is a scalar function whose values are known at the nodes +and :math:`\bar{u}_i` is the average value at the center of cell :math:`i`, +then: + +.. math:: + \bar{u}_i = \frac{u_{i-\tfrac{1}{2}} + u_{i+\tfrac{1}{2}}}{2} + +Our goal is to construct a matrix :math:`A` that averages the values +at the nodes and places them at the cell centers, i.e.: + +.. math:: + \bar{\boldsymbol{u}} = A \, \boldsymbol{u} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u(x)` at the nodes, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the averages at cell centers. + +For the entire mesh, the averaging matrix is given by: + +.. math:: + A = \frac{1}{2} \begin{bmatrix} + 1 & 1 & 0 & 0 & \cdots & 0 & 0 \\ + 0 & 1 & 1 & 0 & \cdots & 0 & 0 \\ + 0 & 0 & 1 &1 & \cdots & 0 & 0 \\ + \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ + 0 & 0 & 0 & 0 & \cdots & 1 & 1 + \end{bmatrix} + :label: operators_averaging_n2c_1d + +where :math:`A` is a sparse matrix. Defining :math:`nc` as the number of cells in the mesh, +:math:`A` is has shape :math:`nc` by :math:`nc \! + \! 1`. + +Cell Centers to Nodes +^^^^^^^^^^^^^^^^^^^^^ + +Let us re-examine the figure illustrating the 1D mesh and assume we know the value of the function :math:`u(x)` at cell centers. +Note that the nodes are not equal distance from the cell centers on either side. +Therefore we cannot simply sum the values and divide by half. Instead we need to implement a weighted averaging. + +If :math:`u(x)` is a scalar function whose values are known at the cell centers +and :math:`\bar{u}_i` is the average value at the nodes, +then: + +.. math:: + \bar{u}_{i+\frac{1}{2}} = \Bigg ( \frac{\Delta x_{i+1}}{\Delta x_i + \Delta x_{i+1}} \Bigg ) u_{i} + + \Bigg ( \frac{\Delta x_i}{\Delta x_i + \Delta x_{i+1}} \Bigg ) u_{i+1} + +Our goal is to construct a matrix :math:`\bar{A}` that averages the values +at the cell centers and places them at the nodes, i.e.: + +.. math:: + \bar{\boldsymbol{u}} = \bar{\boldsymbol{A}} \, \boldsymbol{u} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u(x)` at cell centers, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the averages at the nodes. + +For the entire mesh, the averaging matrix is given by: + +.. math:: + \bar{A} = \frac{1}{2} \begin{bmatrix} + 1 & 0 & 0 & 0 & \cdots & 0 & 0 \\ + a_1 & b_1 & 0 & 0 & \cdots & 0 & 0 \\ + 0 & a_2 & b_2 & 0 & \cdots & 0 & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots \\ + 0 & 0 & 0 & 0 & \cdots & a_{nc-1} & b_{nc-1} \\ + 0 & 0 & 0 & 0 & \cdots & 0 & 1 + \end{bmatrix} \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; + \begin{split} + a_i &= \frac{\Delta x_{i+1}}{\Delta x_i + \Delta x_{i+1}} \\ + & \\ + b_i &= \frac{\Delta x_i}{\Delta x_i + \Delta x_{i+1}} + \end{split} + :label: operators_averaging_c2n_1d + +where :math:`\bar{A}` is a sparse matrix. Defining :math:`nc` as the number of cells in the mesh, +:math:`\bar{A}` has shape :math:`nc \! + \! 1` by :math:`nc`. Note that :math:`\bar{A}_{0,0}` and :math:`\bar{A}_{nc,nc-1}` +are equal to 1. This is because cell center locations needed to compute the average lie outside the mesh +and we must extrapolate using the nearest neighbour. + + +Averaging Scalars in 2D and 3D +============================== + +Nodes to Cell Centers +^^^^^^^^^^^^^^^^^^^^^ + +For a 2D mesh, the value of the function at 4 locations is needed to average from nodes to cell centers. +Let us define a 2D mesh where cell center locations :math:`(x_i, y_j)` are represented using indices :math:`(i,j)`. +The widths of the cells in :math:`x` and :math:`y` are given by :math:`\Delta x_i` and :math:`\Delta y_j`, respectively. + +.. figure:: ../../images/averaging_2d.png + :align: center + :width: 300 + + A 2D tensor mesh which shows the indexing for node and cell center locations. + +If :math:`u(x,y)` is a scalar function whose values are known at the nodes +and :math:`\bar{u} (i,j)` is the average at the center of cell :math:`i,j`, +then: + +.. math:: + \bar{u}(i,j) = \frac{1}{4} \Big [ + u \Big ( i-\tfrac{1}{2}, j-\tfrac{1}{2} \Big ) + + u \Big ( i+\tfrac{1}{2}, j-\tfrac{1}{2} \Big ) + + u \Big ( i-\tfrac{1}{2}, j+\tfrac{1}{2} \Big ) + + u \Big ( i+\tfrac{1}{2}, j+\tfrac{1}{2} \Big ) \Big ] + +Our goal is to construct a matrix :math:`\boldsymbol{A}` that averages the values +at the nodes and places them at the cell centers, i.e.: + +.. math:: + \bar{\boldsymbol{u}} = \boldsymbol{A \, u} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u(x,y)` at the nodes, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the averages at cell centers. + +For tensor meshes, the averaging matrix is rather easy to construct. +Using equation :eq:`operators_averaging_n2c_1d`, the number of cells in the x-direction +can be used to construct a matrix :math:`A_x`. And the number of cells in the y-direction +can be used to construct a matrix :math:`A_y`. The averaging matrix in 2D is given by: + +.. math:: + \boldsymbol{A} = A_y \otimes A_x + +where :math:`\otimes` is the `Kronecker product `__. For a 3D tensor mesh, the averaging matrix +from nodes to cell centers would be given by: + +.. math:: + \boldsymbol{A} = A_z \otimes (A_y \otimes A_x) + + +Cell Centers to Nodes +^^^^^^^^^^^^^^^^^^^^^ + +A nearly identical approach can be implemented to average from cell centers to nodes. +In this case, expression :eq:`operators_averaging_c2n_1d` is used to construct the 1D averaging +matricies in the :math:`x`, :math:`y` (and :math:`z`) directions using the dimensions of the cells +along each axis. Once again, nearest neighbour is used to assign a value to cell centers which lie outside the mesh. + +Our goal is to construct a matrix :math:`\boldsymbol{A}` that averages the values +at the cell centers and places them at the nodes, i.e.: + +.. math:: + \bar{\boldsymbol{u}} = \boldsymbol{A \, u} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u(x,y)` at the cell centers, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the averages at nodes. + +For 2D averaging from cell centers to nodes, we use equation :eq:`operators_averaging_c2n_1d` and the cell widths +in the x and y directions to construct 1D averaging matrices :math:`\bar{A}_x` and :math:`\bar{A}_y`, respectively. +The averaging operator for a 2D tensor mesh is given by: + +.. math:: + \boldsymbol{A} = \bar{A}_y \otimes \bar{A}_x + +And for 3D averaging from cell centers to nodes: + +.. math:: + \boldsymbol{A} = \bar{A}_z \otimes (\bar{A}_y \otimes \bar{A}_x) + + +Faces to Cell Centers +^^^^^^^^^^^^^^^^^^^^^ + +Let us define a 2D mesh where cell center locations :math:`(x_i, y_j)` are represented using indices :math:`(i,j)`. +The widths of the cells in :math:`x` and :math:`y` are given by :math:`\Delta x_i` and :math:`\Delta y_j`, respectively. + +.. figure:: ../../images/averaging_2d_faces.png + :align: center + :width: 350 + + A 2D tensor mesh which shows the indexing for face and cell center locations. + +If :math:`u(x,y)` is a scalar quantity whose values are known on the faces. +and :math:`\bar{u}(i,j)` is the average at the center of cell :math:`i,j`, +then: + +.. math:: + \bar{u}(i,j) = \frac{1}{4} \Big [ + u \Big ( i-\tfrac{1}{2}, j \Big ) + + u \Big ( i+\tfrac{1}{2}, j \Big ) + + u \Big ( i, j-\tfrac{1}{2} \Big ) + + u \Big ( i, j+\tfrac{1}{2} \Big ) \Big ] + +Our goal is to construct a matrix :math:`\boldsymbol{A}` that averages the values +on the faces and places them at the cell centers, i.e.: + +.. math:: + \bar{\boldsymbol{u}} = \boldsymbol{A \, u} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u(x,y)` on the x and y-faces, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the averages at cell centers. + +Let :math:`I_n` be an :math:`n` by :math:`n` identity matrix. +And use equation :eq:`operators_averaging_n2c_1d` to construct 1D averaging matrices :math:`A_x` and :math:`A_y`. +Then for a 2D tensor mesh, the averaging matrix has the form: + +.. math:: + \boldsymbol{A} = \frac{1}{2} \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{A_y} \end{bmatrix} + +where + +.. math:: + \begin{align} + \boldsymbol{A_x} &= I_{ny} \otimes A_x \\ + \boldsymbol{A_y} &= A_y \otimes I_{nx} + \end{align} + :label: operators_averaging_Ai_f2c_2d + +For a 3D tensor mesh, the averaging matrix takes the form: + +.. math:: + \boldsymbol{A} = \frac{1}{3} \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{A_y} & \boldsymbol{A_z} \end{bmatrix} + +where + +.. math:: + \begin{align} + \boldsymbol{A_x} &= I_{nz} \otimes ( I_{ny} \otimes A_x ) \\ + \boldsymbol{A_y} &= I_{nz} \otimes ( A_y \otimes I_{nx} ) \\ + \boldsymbol{A_z} &= A_z \otimes ( I_{ny} \otimes I_{nx} ) + \end{align} + :label: operators_averaging_Ai_f2c_3d + + +Edges to Cell Centers +^^^^^^^^^^^^^^^^^^^^^ + +Let us define a 2D mesh where cell center locations :math:`(x_i, y_j)` are represented using indices :math:`(i,j)`. +The widths of the cells in :math:`x` and :math:`y` are given by :math:`\Delta x_i` and :math:`\Delta y_j`, respectively. + +.. figure:: ../../images/averaging_2d_edges.png + :align: center + :width: 350 + + A 2D tensor mesh which shows the indexing for edge and cell center locations. + +If :math:`u(x,y)` is a scalar quantity whose values are known on the edges. +and :math:`\bar{u}(i,j)` is the average at the center of cell :math:`i,j`, +then: + +.. math:: + \bar{u}(i,j) = \frac{1}{4} \Big [ + u \Big ( i, j-\tfrac{1}{2} \Big ) + + u \Big ( i, j+\tfrac{1}{2} \Big ) + + u \Big ( i-\tfrac{1}{2}, j \Big ) + + u \Big ( i+\tfrac{1}{2}, j \Big ) \Big ] + +Our goal is to construct a matrix :math:`\boldsymbol{A}` that averages the values +on the edges and places them at the cell centers, i.e.: + +.. math:: + \bar{\boldsymbol{u}} = \boldsymbol{A \, u} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u(x,y)` on the x and y-edges, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the averages at cell centers. + +Let :math:`I_n` be an :math:`n` by :math:`n` identity matrix. +And use equation :eq:`operators_averaging_n2c_1d` to construct 1D averaging matrices :math:`A_x` and :math:`A_y`. +Then for a 2D tensor mesh, the averaging matrix has the form: + +.. math:: + \boldsymbol{A} = \frac{1}{2} \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{A_y} \end{bmatrix} + +where + +.. math:: + \begin{align} + \boldsymbol{A_x} &= A_x \otimes I_{ny} \\ + \boldsymbol{A_y} &= I_{nx} \otimes A_y + \end{align} + :label: operators_averaging_Ai_e2c_2d + +For a 3D tensor mesh, the averaging matrix takes the form: + +.. math:: + \boldsymbol{A} = \frac{1}{3} \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{A_y} & \boldsymbol{A_z} \end{bmatrix} + +where + +.. math:: + \begin{align} + \boldsymbol{A_x} &= I_{nz} \otimes ( A_y \otimes A_x ) \\ + \boldsymbol{A_y} &= A_z \otimes ( I_{ny} \otimes A_x ) \\ + \boldsymbol{A_z} &= A_z \otimes ( A_y \otimes I_{nx} ) + \end{align} + :label: operators_averaging_Ai_e2c_3d + + +Averaging Vectors in 2D and 3D +============================== + +Faces to Cell Centers +^^^^^^^^^^^^^^^^^^^^^ + +Let :math:`\vec{u}(x,y)` be a vector function that is known on the faces. +That is, :math:`u_x (x,y)` lives on x-faces and :math:`u_y(x,y)` lives on y-faces. +In this case, the x-faces are used to average the x-component +to cell centers and the y-faces are used to average the y-component to cell centers separately. + +Let us define a 2D mesh where cell center locations :math:`(x_i, y_j)` are represented using indices :math:`(i,j)`. +The widths of the cells in :math:`x` and :math:`y` are given by :math:`\Delta x_i` and :math:`\Delta y_j`, respectively. + +.. figure:: ../../images/averaging_2d_faces.png + :align: center + :width: 350 + + A 2D tensor mesh which shows the indexing for face and cell center locations. + +Where :math:`\bar{u}_x (i,j)` is the average x-component at the center of cell :math:`i,j`: + +.. math:: + \bar{u}_x (i,j) = \frac{1}{2} \Big [ u_x \Big ( i-\tfrac{1}{2},j \Big ) + u_x \Big ( i+\tfrac{1}{2},j \Big ) \Big ] + +And where :math:`\bar{u}_y (i,j)` is the average at the center of cell :math:`i,j`: + +.. math:: + \bar{u}_y (i,j) = \frac{1}{2} \Big [ u_y \Big ( i,j-\tfrac{1}{2} \Big ) + u_y \Big ( i,j+\tfrac{1}{2} \Big ) \Big ] + +Our goal is to construct a matrix :math:`\boldsymbol{A}` that averages the vector components living +on the faces separately and places them at the cell centers. +Here, the operation :math:`\bar{\boldsymbol{u}} = \boldsymbol{A \, u}` takes the form: + +.. math:: + \begin{bmatrix} \bar{\boldsymbol{u}}_{\boldsymbol{x}} \\ \bar{\boldsymbol{u}}_{\boldsymbol{y}} \end{bmatrix} = + \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{A_y} \end{bmatrix} + \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \end{bmatrix} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u_x(x,y)` and :math:`u_y(x,y)` on their respective faces, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the x component and y component averages at cell centers. +Matrices :math:`\boldsymbol{A_x}` and :math:`\boldsymbol{A_y}` are defined in expression :eq:`operators_averaging_Ai_f2c_2d`. + +In 3D, the corresponding averaging matrix is defined by: + +.. math:: + \begin{bmatrix} \bar{\boldsymbol{u}}_{\boldsymbol{x}} \\ \bar{\boldsymbol{u}}_{\boldsymbol{y}} \\ \bar{\boldsymbol{u}}_{\boldsymbol{z}} \end{bmatrix} = + \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{0} & \boldsymbol{0} \\ + \boldsymbol{0} & \boldsymbol{A_y} & \boldsymbol{0} \\ + \boldsymbol{0} & \boldsymbol{0} & \boldsymbol{A_z} \end{bmatrix} + \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} + +where matrices :math:`\boldsymbol{A_x}`, :math:`\boldsymbol{A_y}` and :math:`\boldsymbol{A_z}` are defined in expression :eq:`operators_averaging_Ai_f2c_3d`. + + +Edges to Cell Centers +^^^^^^^^^^^^^^^^^^^^^ + +Let :math:`\vec{u}(x,y)` be a vector function that is known on the edges. +That is, :math:`u_x (x,y)` lives on x-edges and :math:`u_y(x,y)` lives on y-edges. +In this case, the x-edges are used to average the x-component +to cell centers and the y-edges are used to average the y-component to cell centers separately. + +Let us define a 2D mesh where cell center locations :math:`(x_i, y_j)` are represented using indices :math:`(i,j)`. +The widths of the cells in :math:`x` and :math:`y` are given by :math:`\Delta x_i` and :math:`\Delta y_j`, respectively. + +.. figure:: ../../images/averaging_2d_edges.png + :align: center + :width: 350 + + A 2D tensor mesh which shows the indexing for edge and cell center locations. + +Where :math:`\bar{u}_x (i,j)` is the average x-component at the center of cell :math:`i,j`: + +.. math:: + \bar{u}_x (i,j) = \frac{1}{2} \Big [ u_x \Big ( i,j-\tfrac{1}{2} \Big ) + u_x \Big ( i,j+\tfrac{1}{2} \Big ) \Big ] + +And where :math:`\bar{u}_y (i,j)` is the average at the center of cell :math:`i,j`: + +.. math:: + \bar{u}_y (i,j) = \frac{1}{2} \Big [ u_y \Big ( i-\tfrac{1}{2},j \Big ) + u_y \Big ( i+\tfrac{1}{2},j \Big ) \Big ] + +Our goal is to construct a matrix :math:`\boldsymbol{A}` that averages the vector components living +on the edges separately and places them at the cell centers. +Here, the operation :math:`\bar{\boldsymbol{u}} = \boldsymbol{A \, u}` takes the form: + +.. math:: + \begin{bmatrix} \bar{\boldsymbol{u}}_{\boldsymbol{x}} \\ \bar{\boldsymbol{u}}_{\boldsymbol{y}} \end{bmatrix} = + \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{A_y} \end{bmatrix} + \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \end{bmatrix} + +where :math:`\boldsymbol{u}` is a vector that stores the known values of :math:`u_x(x,y)` and :math:`u_y(x,y)` on their respective edges, +and :math:`\bar{\boldsymbol{u}}` is a vector that stores the x component and y component averages at cell centers. +Matrices :math:`\boldsymbol{A_x}` and :math:`\boldsymbol{A_y}` are defined in expression :eq:`operators_averaging_Ai_e2c_2d`. + +In 3D, the corresponding averaging matrix is defined by: + +.. math:: + \begin{bmatrix} \bar{\boldsymbol{u}}_{\boldsymbol{x}} \\ \bar{\boldsymbol{u}}_{\boldsymbol{y}} \\ \bar{\boldsymbol{u}}_{\boldsymbol{z}} \end{bmatrix} = + \begin{bmatrix} \boldsymbol{A_x} & \boldsymbol{0} & \boldsymbol{0} \\ + \boldsymbol{0} & \boldsymbol{A_y} & \boldsymbol{0} \\ + \boldsymbol{0} & \boldsymbol{0} & \boldsymbol{A_z} \end{bmatrix} + \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} + +where matrices :math:`\boldsymbol{A_x}`, :math:`\boldsymbol{A_y}` and :math:`\boldsymbol{A_z}` are defined in expression :eq:`operators_averaging_Ai_e2c_3d`. + + + + diff --git a/docs/content/theory/operators_differential.rst b/docs/content/theory/operators_differential.rst new file mode 100644 index 000000000..e1d89e871 --- /dev/null +++ b/docs/content/theory/operators_differential.rst @@ -0,0 +1,247 @@ +.. _operators_differential: + +Differential Operators +********************** + +Here, we provide the background theory for how discrete differential operators are formed. +We follow the approach discussed in :cite:`haber2014,HymanShashkov1999`. +For geophysical problems, the relationship between two physical quantities may include one of several differential operators: + + - **Divergence:** :math:`\nabla \cdot \vec{u} = \dfrac{\partial u_x}{\partial x} + \dfrac{\partial u_y}{\partial y} + \dfrac{\partial u_y}{\partial y}` + - **Gradient:** :math:`\nabla \phi = \dfrac{\partial \phi}{\partial x}\hat{x} + \dfrac{\partial \phi}{\partial y}\hat{y} + \dfrac{\partial \phi}{\partial z}\hat{z}` + - **Curl:** :math:`\nabla \times \vec{u} = \Bigg ( \dfrac{\partial u_y}{\partial z} - \dfrac{\partial u_z}{\partial y} \Bigg )\hat{x} - \Bigg ( \dfrac{\partial u_x}{\partial z} - \dfrac{\partial u_z}{\partial x} \Bigg )\hat{y} + \Bigg ( \dfrac{\partial u_x}{\partial y} - \dfrac{\partial u_y}{\partial x} \Bigg )\hat{z}` + +When implementing the finite volume method, continuous variables are discretized to live at the cell centers, nodes, edges or faces of a mesh. +Thus for each differential operator, we need a discrete approximation that acts on the discrete variables living on the mesh. + +Our approximations are derived using numerical differentiation (figure below). So long as a function :math:`f(x)` is sufficiently smooth +within the interval :math:`[x-h/2, \; x+h/2]`, then the derivative of the function at :math:`x` is approximated by: + +.. math:: + \frac{df(x)}{dx} \approx \frac{f(x+h/2) \; - \; f(x-h/2)}{h} + +where the approximation becomes increasingly accurate as :math:`h \rightarrow 0`. In subsequent sections, we will show how +the gradient, divergence and curl can be computed for discrete variables. + +.. figure:: ../../images/approximate_derivative.png + :align: center + :width: 300 + + Approximating the derivative of :math:`f(x)` using numerical differentiation. + + +**Tutorial:** :ref:`tutorial for constructing and applying differential operators ` + + +.. _operators_differential_divergence: + +Divergence +---------- + +Let us define a continuous scalar function :math:`\phi` and a continuous vector function :math:`\vec{u}` such that: + +.. math:: + \phi = \nabla \cdot \vec{u} + +And let :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` be the discrete representations of :math:`\phi` and :math:`\vec{u}` +that live on the mesh (centers, nodes, edges or faces), respectively. Provided we know the discrete values :math:`\boldsymbol{u}`, +our goal is to use discrete differentiation to approximate the values of :math:`\boldsymbol{\phi}`. +We begin by considering a single cell (2D or 3D). We let the indices :math:`i`, :math:`j` and :math:`k` +denote positions along the x, y and z axes, respectively. + +.. figure:: ../../images/divergence_discretization.png + :align: center + :width: 600 + + Discretization for approximating the divergence at the center of a single 2D cell (left) and 3D cell (right). + ++-------------+-------------------------------------------------+-----------------------------------------------------+ +| | **2D** | **3D** | ++-------------+-------------------------------------------------+-----------------------------------------------------+ +| **center** | :math:`(i,j)` | :math:`(i,j,k)` | ++-------------+-------------------------------------------------+-----------------------------------------------------+ +| **x-faces** | :math:`(i-\frac{1}{2},j)\;\; (i+\frac{1}{2},j)` | :math:`(i-\frac{1}{2},j,k)\;\; (i+\frac{1}{2},j,k)` | ++-------------+-------------------------------------------------+-----------------------------------------------------+ +| **y-faces** | :math:`(i,j-\frac{1}{2})\;\; (i,j+\frac{1}{2})` | :math:`(i,j-\frac{1}{2},k)\;\; (i,j+\frac{1}{2},k)` | ++-------------+-------------------------------------------------+-----------------------------------------------------+ +| **z-faces** | N/A | :math:`(i,j,k-\frac{1}{2})\;\; (i,j,k+\frac{1}{2})` | ++-------------+-------------------------------------------------+-----------------------------------------------------+ + +As we will see, it makes the most sense for :math:`\boldsymbol{\phi}` to live at the cell centers and +for the components of :math:`\boldsymbol{u}` to live on the faces. If :math:`u_x` lives on x-faces, then its discrete +derivative with respect to :math:`x` lives at the cell center. And if :math:`u_y` lives on y-faces its discrete +derivative with respect to :math:`y` lives at the cell center. Likewise for :math:`u_z`. Thus to approximate the +divergence of :math:`\vec{u}` at the cell center, we simply need to sum the discrete derivatives of :math:`u_x`, :math:`u_y` +and :math:`u_z` that are defined at the cell center. Where :math:`h_x`, :math:`h_y` and :math:`h_z` represent the dimension of the cell along the x, y and +z directions, respectively: + +.. math:: + \begin{align} + \mathbf{In \; 2D:} \;\; \phi(i,j) \approx \; & \frac{u_x(i,j+\frac{1}{2}) - u_x(i,j-\frac{1}{2})}{h_x} \\ + & + \frac{u_y(i+\frac{1}{2},j) - u_y(i-\frac{1}{2},j)}{h_y} + \end{align} + +| + +.. math:: + \begin{align} + \mathbf{In \; 3D:} \;\; \phi(i,j,k) \approx \; & \frac{u_x(i+\frac{1}{2},j,k) - u_x(i-\frac{1}{2},j,k)}{h_x} \\ + & + \frac{u_y(i,j+\frac{1}{2},k) - u_y(i,j-\frac{1}{2},k)}{h_y} \\ + & + \frac{u_z(i,j,k+\frac{1}{2}) - u_z(i,j,k-\frac{1}{2})}{h_z} + \end{align} + + +Ultimately we are trying to approximate the divergence at the center of every cell in a mesh. +Adjacent cells share faces. If the components :math:`u_x`, :math:`u_y` and :math:`u_z` are +continuous across their respective faces, then :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` +can be related by a sparse matrix-vector product: + +.. math:: + \boldsymbol{\phi} = \boldsymbol{D \, u} + +where :math:`\boldsymbol{D}` is the divergence matrix from faces to cell centers, +:math:`\boldsymbol{\phi}` is a vector containing the discrete approximations of :math:`\phi` at all cell centers, +and :math:`\boldsymbol{u}` stores the components of :math:`\vec{u}` on cell faces as a vector of the form: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} + +.. _operators_differential_gradient: + +Gradient +-------- + +Let us define a continuous scalar function :math:`\phi` and a continuous vector function :math:`\vec{u}` such that: + +.. math:: + \vec{u} = \nabla \phi + +And let :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` be the discrete representations of :math:`\phi` and :math:`\vec{u}` +that live on the mesh (centers, nodes, edges or faces), respectively. Provided we know the discrete values :math:`\boldsymbol{\phi}`, +our goal is to use discrete differentiation to approximate the vector components of :math:`\boldsymbol{u}`. +We begin by considering a single cell (2D or 3D). We let the indices :math:`i`, :math:`j` and :math:`k` +denote positions along the x, y and z axes, respectively. + +.. figure:: ../../images/gradient_discretization.png + :align: center + :width: 600 + + Discretization for approximating the gradient on the edges of a single 2D cell (left) and 3D cell (right). + +As we will see, it makes the most sense for :math:`\boldsymbol{\phi}` to live at the cell nodes and +for the components of :math:`\boldsymbol{u}` to live on corresponding edges. If :math:`\phi` lives on the nodes, then: + + - the partial derivative :math:`\dfrac{\partial \phi}{\partial x}\hat{x}` lives on x-edges, + - the partial derivative :math:`\dfrac{\partial \phi}{\partial y}\hat{y}` lives on y-edges, and + - the partial derivative :math:`\dfrac{\partial \phi}{\partial z}\hat{z}` lives on z-edges + +Thus to approximate the gradient of :math:`\phi`, +we simply need to take discrete derivatives of :math:`\phi` with respect to :math:`x`, :math:`y` and :math:`z`, +and organize the resulting vector components on the corresponding edges. +Let :math:`h_x`, :math:`h_y` and :math:`h_z` represent the dimension of the cell along the x, y and +z directions, respectively. + +**In 2D**, the value of :math:`\phi` at 4 node locations is used to approximate the vector components of the +gradient at 4 edges locations (2 x-edges and 2 y-edges) as follows: + +.. math:: + \begin{align} + u_x \Big ( i+\frac{1}{2},j \Big ) \approx \; & \frac{\phi (i+1,j) - \phi (i,j)}{h_x} \\ + u_x \Big ( i+\frac{1}{2},j+1 \Big ) \approx \; & \frac{\phi (i+1,j+1) - \phi (i,j+1)}{h_x} \\ + u_y \Big ( i,j+\frac{1}{2} \Big ) \approx \; & \frac{\phi (i,j+1) - \phi (i,j)}{h_y} \\ + u_y \Big ( i+1,j+\frac{1}{2} \Big ) \approx \; & \frac{\phi (i+1,j+1) - \phi (i+1,j)}{h_y} + \end{align} + +**In 3D**, the value of :math:`\phi` at 8 node locations is used to approximate the vector components of the +gradient at 12 edges locations (4 x-edges, 4 y-edges and 4 z-edges). An example of the approximation +for each vector component is given below: + +.. math:: + \begin{align} + u_x \Big ( i+\frac{1}{2},j,k \Big ) \approx \; & \frac{\phi (i+1,j,k) - \phi (i,j,k)}{h_x} \\ + u_y \Big ( i,j+\frac{1}{2},k \Big ) \approx \; & \frac{\phi (i,j+1,k) - \phi (i,j,k)}{h_y} \\ + u_z \Big ( i,j,k+\frac{1}{2} \Big ) \approx \; & \frac{\phi (i,j,k+1) - \phi (i,j,k)}{h_z} + \end{align} + + +Ultimately we are trying to approximate the vector components of the gradient at all edges of a mesh. +Adjacent cells share nodes. If :math:`\phi` is continuous at the nodes, then :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` +can be related by a sparse matrix-vector product: + +.. math:: + \boldsymbol{u} = \boldsymbol{G \, \phi} + +where :math:`\boldsymbol{G}` is the gradient matrix that maps from nodes to edges, +:math:`\boldsymbol{\phi}` is a vector containing :math:`\phi` at all nodes, +and :math:`\boldsymbol{u}` stores the components of :math:`\vec{u}` on cell edges as a vector of the form: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} + +.. _operators_differential_curl: + +Curl +---- + +Let us define two continuous vector functions :math:`\vec{u}` and :math:`\vec{w}` such that: + +.. math:: + \vec{w} = \nabla \times \vec{u} + +And let :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` be the discrete representations of :math:`\vec{u}` and :math:`\vec{w}` +that live on the mesh (centers, nodes, edges or faces), respectively. Provided we know the discrete values :math:`\boldsymbol{u}`, +our goal is to use discrete differentiation to approximate the vector components of :math:`\boldsymbol{w}`. +We begin by considering a single 3D cell. We let the indices :math:`i`, :math:`j` and :math:`k` +denote positions along the x, y and z axes, respectively. + +.. figure:: ../../images/curl_discretization.png + :align: center + :width: 800 + + Discretization for approximating the x, y and z components of the curl on the respective faces of a 3D cell. + + +As we will see, it makes the most sense for the vector components of :math:`\boldsymbol{u}` to live on the edges +for the vector components of :math:`\boldsymbol{w}` to live the faces. In this case, we need to approximate: + + + - the partial derivatives :math:`\dfrac{\partial u_y}{\partial z}` and :math:`\dfrac{\partial u_z}{\partial y}` to compute :math:`w_x`, + - the partial derivatives :math:`\dfrac{\partial u_x}{\partial z}` and :math:`\dfrac{\partial u_z}{\partial x}` to compute :math:`w_y`, and + - the partial derivatives :math:`\dfrac{\partial u_x}{\partial y}` and :math:`\dfrac{\partial u_y}{\partial x}` to compute :math:`w_z` + +**In 3D**, discrete values at 12 edge locations (4 x-edges, 4 y-edges and 4 z-edges) are used to +approximate the vector components of the curl at 6 face locations (2 x-faces, 2-faces and 2 z-faces). +An example of the approximation for each vector component is given below: + +.. math:: + \begin{align} + w_x \Big ( i,j \! +\!\!\frac{1}{2},k \! +\!\!\frac{1}{2} \Big ) \!\approx\! \; & + \!\Bigg ( \! \frac{u_z (i,j \! +\!\!1,k \! +\!\!\frac{1}{2}) \! -\! u_z (i,j,k \! +\!\!\frac{1}{2})}{h_y} \Bigg) \! + \! -\! \!\Bigg ( \! \frac{u_y (i,j \! +\!\!\frac{1}{2},k \! +\!\!1) \! -\! u_y (i,j \! +\!\!\frac{1}{2},k)}{h_z} \Bigg) \! \\ + & \\ + w_y \Big ( i \! +\!\!\frac{1}{2},j,k \! +\!\!\frac{1}{2} \Big ) \!\approx\! \; & + \!\Bigg ( \! \frac{u_x (i \! +\!\!\frac{1}{2},j,k \! +\!\!1) \! -\! u_x (i \! +\!\!\frac{1}{2},j,k)}{h_z} \Bigg) + \! -\! \!\Bigg ( \! \frac{u_z (i \! +\!\!1,j,k \! +\!\!\frac{1}{2}) \! -\! u_z (i,j,k \! +\!\!\frac{1}{2})}{h_x} \Bigg) \! \\ + & \\ + w_z \Big ( i \! +\!\!\frac{1}{2},j \! +\!\!\frac{1}{2},k \Big ) \!\approx\! \; & + \!\Bigg ( \! \frac{u_y (i \! +\!\!1,j \! +\!\!\frac{1}{2},k) \! -\! u_y (i,j \! +\!\!\frac{1}{2},k)}{h_x} \Bigg ) + \! -\! \!\Bigg ( \! \frac{u_x (i \! +\!\!\frac{1}{2},j \! +\!\!1,k) \! -\! u_x (i \! +\!\!\frac{1}{2},j,k)}{h_y} \Bigg) \! + \end{align} + + +Ultimately we are trying to approximate the curl on all the faces within a mesh. +Adjacent cells share edges. If the components :math:`u_x`, :math:`u_y` and :math:`u_z` are +continuous across at the edges, then :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` +can be related by a sparse matrix-vector product: + +.. math:: + \boldsymbol{w} = \boldsymbol{C \, u} + +where :math:`\boldsymbol{C}` is the curl matrix from edges to faces, +:math:`\boldsymbol{u}` is a vector that stores the components of :math:`\vec{u}` on cell edges, +and :math:`\boldsymbol{w}` is a vector that stores the components of :math:`\vec{w}` on cell faces such that: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} + \;\;\;\; \textrm{and} \;\;\;\; \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_z} \end{bmatrix} + diff --git a/docs/content/theory/operators_index.rst b/docs/content/theory/operators_index.rst new file mode 100644 index 000000000..2e7b3cf78 --- /dev/null +++ b/docs/content/theory/operators_index.rst @@ -0,0 +1,39 @@ +.. _operators_index: + +Operators +********* + +To approximate numerical solutions to partial differential equations using the finite volume method, +we must be able to construct discrete approximations to the following mathematical operations: + + - Interpolation + - Averaging + - Differential (gradient, divergence, curl) + +Let any one of the aforementioned operations be defined as a mapping such that :math:`w = \mathbb{M}[u]`. +Where :math:`\boldsymbol{u}` is a vector containing the discrete representation of :math:`u` on the mesh, +and :math:`\boldsymbol{w}` is a vector containing the discrete represention of :math:`w` on the mesh, +we can approximate the mapping as: + +.. math:: + \boldsymbol{w} \approx \boldsymbol{M \, u} + +where :math:`\boldsymbol{M}` is a sparse matrix. Thus for each operator, the mapping is approximated by +constructing a sparse matrix and performing a matrix-vector product. Subsequent sections are devoted to +the general formation of these matrices. + + +**Contents:** + +.. toctree:: + :maxdepth: 1 + + operators_interpolation + operators_averaging + operators_differential + +**Tutorials:** + +- :ref:`Interpolation Operators ` +- :ref:`Averaging Operators ` +- :ref:`Differential Operators ` \ No newline at end of file diff --git a/docs/content/theory/operators_interpolation.rst b/docs/content/theory/operators_interpolation.rst new file mode 100644 index 000000000..747fdc5b0 --- /dev/null +++ b/docs/content/theory/operators_interpolation.rst @@ -0,0 +1,258 @@ +.. _operators_interpolation: + +Interpolation +************* + +Interpolation is required when a discrete quantity is known on the mesh (centers, nodes, edges or faces), +but we would like to estimate its value at locations within the continuous domain. +Here, we discuss how a sparse matrix :math:`\boldsymbol{P}` can be formed which interpolates the discrete values to +a set of locations in continuous space. Where :math:`\boldsymbol{u}` is vector that stores +the values of a discrete quantity on the mesh (centers, nodes, faces or edges) and +:math:`\boldsymbol{w}` is a vector containing the interpolated quantity at a set of locations, +we look to construct an interpolation matrix such that: + +.. math:: + \boldsymbol{w} = \boldsymbol{P \, u} + +Presently there is an extensive set of interpolation methods (e.g. polynomial, spline, piecewise constant). +One of the most effective and widely used interpolation methods is linear interpolation. +The *discretize* package primarily uses linear interpolation because 1) it is very fast, and 2) higher order +interpolation methods require the construction of matricies which are less sparse. +The formulation for linear interpolation is adequately presented on Wikipedia, see: + + - `Linear Interpolation (1D) `__ + - `Bilinear Interpolation (2D) `__ + - `Trilinear Interpolation (3D) `__ + +**Tutorial:** :ref:`tutorial for constructing and applying interpolation operators ` + +Interpolation Matrix in 1D +========================== + +Let us define a 1D mesh that contains 8 cells of arbitrary width. +The mesh is illustrated in the figure below. The width of each cell is +defined as :math:`h_i`. The location of each node is defined as :math:`x_i`. + +.. figure:: ../../images/interpolation_1d.png + :align: center + :width: 600 + :name: operators_interpolation_1d + + Tensor mesh in 1D. + +Now let :math:`u(x)` be a function whose values are known at the nodes; +i.e. :math:`u_i = u(x_i)`. +The approximate value of the function at location :math:`x^*` +using linear interpolation is given by: + +.. math:: + u(x^*) \approx u_3 + \Bigg ( \frac{u_4 - u_3}{h_3} \Bigg ) (x^* - x_3) + :label: operators_averaging_interpolation_1d + + +Suppose now that we organize the known values of :math:`u(x)` at the nodes +into a vector of the form: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} u_0 & u_1 & u_2 & u_3 & u_4 & u_5 & u_6 & u_7 & u_8 \end{bmatrix}^T + +If we define a row: + +.. math:: + \boldsymbol{p_0} = \begin{bmatrix} 0 & 0 & 0 & a_3 & a_4 & 0 & 0 & 0 & 0 \end{bmatrix} + +where + +.. math:: + a_3 = 1 - \frac{x^* - x_3}{h_3} \;\;\;\;\; \textrm{and} \;\;\;\;\; a_4 = \frac{x^* - x_3}{h_3} + +then + +.. math:: + u(x^*) \approx \boldsymbol{p_0 \, u} + +For a single location, we have just seen how a linear operator can be constructed to +compute the interpolation using a matrix vector-product. + +Now consider the case where you would like to interpolate the function from the nodes to +an arbitrary number of locations within the boundaries of the mesh. +For each location, we simply construct the corresponding row in the interpolation matrix. +Where :math:`\boldsymbol{u^*}` is a vector containing the approximations of :math:`u(x)` at :math:`M` +locations: + +.. math:: + \boldsymbol{u^*} \approx \boldsymbol{P\, u} \;\;\;\;\;\; \textrm{where} \;\;\;\;\;\; + \boldsymbol{P} = \begin{bmatrix} \cdots \;\; \boldsymbol{p_0} \;\; \cdots \\ + \cdots \;\; \boldsymbol{p_1} \;\; \cdots \\ \vdots \\ + \cdots \, \boldsymbol{p_{M-1}} \, \cdots \end{bmatrix} + :label: operators_averaging_interpolation_matrix + +:math:`\boldsymbol{P}` is a sparse matrix whose rows contain a maximum of 2 non-zero elements. +The size of :math:`\boldsymbol{P}` is the number of locations by the number of nodes. +For seven locations (:math:`x^* = 3,1,9,2,5,2,10`) and our mesh (9 nodes), +the non-zero elements of the interpolation matrix are illustrated below. + +.. figure:: ../../images/interpolation_1d_sparse.png + :align: center + :width: 250 + + +**What if the function is defined at cell centers?** + +Here we let :math:`\bar{x}_i` define the center locations +for cells 0 through 7, and we let :math:`\bar{u}_i = u(\bar{x}_i)`. +In this case, the approximation defined in expression :eq:`operators_averaging_interpolation_1d` is replaced by: + +.. math:: + u(x^*) \approx \bar{u}_3 + 2 \Bigg ( \frac{\bar{u}_4 - \bar{u}_3}{h_3 + h_4} \Bigg ) (x^* - \bar{x}_3) + +For an arbitrary number of locations, we can construct an interpolation matrix similar to that shown +in expression :eq:`operators_averaging_interpolation_1d`. In this case however, the size of +:math:`\boldsymbol{P}` is the number of locations by the number of cells. Note that we **cannot** +interpolate at locations between the first or last cell center and the boundaries of the mesh +for quantities defined at cell centers. + + +Interpolation Matrix in 2D and 3D +================================= + +In 1D, the location of the interpolated quantity lies between 2 nodes or cell centers. +In 2D however, the location of the interpolated quantity lies within 4 nodes or cell centers. + +.. figure:: ../../images/interpolation_2d.png + :align: center + :width: 300 + + A tensor mesh in 2D denoting interpolation from nodes (blue) and cell centers (red). + +Let :math:`(x^*, y^*)` be within a cell whose nodes are located at +:math:`(x_1, y_1)`, :math:`(x_2, y_1)`, :math:`(x_1, y_2)` and :math:`(x_2, y_2)`. +If we define :math:`u_0 = u(x_1, y_1)`, :math:`u_1 = u(x_2, y_1)`, :math:`u_2 = u(x_1, y_2)` and +:math:`u_3 = u(x_2, y_2)`, then + +.. math:: + u(x^*, y^*) \approx a_0 u_0 + a_1 u_1 + a_2 u_2 + a_3 u_3 + +where :math:`a_0`, :math:`a_1`, :math:`a_2` and :math:`a_3` are coefficients determined from equations +governing `bilinear interpolation `__ . +These coefficients represent the 4 non-zero values within the corresponding row of the interpolation matrix :math:`\boldsymbol{P}`. + +Where the values of :math:`u(x,y)` at all nodes are organized into a single vector :math:`\boldsymbol{u}`, +and :math:`\boldsymbol{u^*}` is a vector containing the approximations of :math:`u(x,y)` at an arbitrary number of locations: + +.. math:: + \boldsymbol{u^*} \approx \boldsymbol{P\, u} + :label: operators_interpolation_general + +In each row of :math:`\boldsymbol{P}`, the position of the non-zero elements :math:`a_0`, :math:`a_1`, :math:`a_2` and :math:`a_3` +corresponds to the indecies of the 4 nodes comprising a specific cell. +Once again the shape of :math:`\boldsymbol{P}` is the number of locations by the number of nodes. + +**What if the function is defined at cell centers?** + +A similar result can be obtained by interpolating a function define at cell centers. +In this case, we let :math:`(x^*, y^*)` lie within 4 cell centers located at +:math:`(\bar{x}_1, \bar{y}_1)`, :math:`(\bar{x}_2, \bar{y}_1)`, :math:`(\bar{x}_1, \bar{y}_2)` and :math:`(\bar{x}_2, \bar{y}_2)`. + +.. math:: + u(x^*, y^*) \approx a_0 \bar{u}_0 + a_1 \bar{u}_1 + a_2 \bar{u}_2 + a_3 \bar{u}_3 + +The resulting interpolation is defined similar to expression :eq:`operators_interpolation_general`. +However the size of the resulting interpolation matrix is the number of locations by number of cells. + +**What about for 3D case?** + +The derivation for the 3D case is effectively the same, except 8 node or center locations must +be used in the interpolation. Thus: + +.. math:: + u(x^*, y^*, z^*) \approx \sum_{k=0}^7 a_k u_k + +This creates an interpolation matrix :math:`\boldsymbol{P}` with 8 non-zero entries per row. +To learn how to compute the value of the coefficients :math:`a_k`, +see `trilinear interpolation (3D) `__ + +Interpolation of Vectors +======================== + +Scalar quantities are discretized to live at nodes or cell centers, whereas the +components of vectors are discretized to live on their respective faces or edges; +see :ref:`where quantities live `. + +.. figure:: ../../images/interpolation_2d_vectors.png + :align: center + :width: 600 + + A tensor mesh in 2D denoting interpolation from faces (left) and edges (right). + +Let :math:`\vec{u} (x,y)` be a 2D vector function that is known on the faces of the mesh; +that is, :math:`u_x` lives on the x-faces and :math:`u_y` lives on the y-faces. +Note that in the above figure, the x-faces and y-faces both form tensor grids. +If we want to approximate the components of the vector at a location :math:`(x^*,y^*)`, +we simply need to treat each component as a scalar function and interpolate it separately. + +Where :math:`u_{x,i}` represents the x-component of :math:`\vec{u} (x,y)` on a face :math:`i` being used for the interpolation, +the approximation of the x-component at :math:`(x^*, y^*)` has the form: + +.. math:: + u_x(x^*, y^*) \approx a_0 u_{x,0} + a_1 u_{x,1} + a_2 u_{x,2} + a_3 u_{x,3} + :label: operators_interpolation_xvec_coef + +For the the y-component, we have a similar representation: + +.. math:: + u_y(x^*, y^*) \approx b_0 u_{y,0} + b_1 u_{y,1} + b_2 u_{y,2} + b_3 u_{y,3} + +Where :math:`\boldsymbol{u}` is a vector that organizes the discrete components of :math:`\vec{u} (x,y)` on cell faces, +and :math:`\boldsymbol{u^*}` is a vector organizing the components of the approximations of :math:`\vec{u}(x,y)` at an arbitrary number of locations, +the interpolation matrix :math:`\boldsymbol{P}` is defined by: + +.. math:: + \boldsymbol{u^*} \approx \boldsymbol{P \, u} + :label: operators_interpolation_2d_sys + +where + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \end{bmatrix} + \;\;\textrm{,}\;\;\;\; + \boldsymbol{u^*} = \begin{bmatrix} \boldsymbol{u_x^*} \\ \boldsymbol{u_y^*} \end{bmatrix} + \;\;\;\;\textrm{and}\;\;\;\; + \boldsymbol{P} = \begin{bmatrix} \boldsymbol{P_x} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_y} \end{bmatrix} + +The interpolation matrix :math:`\boldsymbol{P}` is a sparse block-diagonal matrix. +The size of the interpolation matrix is the number of locations by the number of faces in the mesh. + +**What if we want to interpolate from edges?** + +In this case, the derivation is effectively the same. +However, the locations used for the interpolation are different and +:math:`\boldsymbol{u}` is now a vector that organizes the discrete components of :math:`\vec{u} (x,y)` on cell edges. + + +**What if we are interpolating a 3D vector?** + +In this case, there are 8 face locations or 8 edge locations that are used to approximate +:math:`\vec{u}(x,y,z)` at each location :math:`(x^*, y^*, z^*)`. +Similar to expression :eq:`operators_interpolation_xvec_coef` we have: + +.. math:: + \begin{align} + u_x(x^*, y^*, z^*) & \approx \sum_{i=1}^7 a_i u_{x,i} \\ + u_y(x^*, y^*, z^*) & \approx \sum_{i=1}^7 b_i u_{y,i} \\ + u_z(x^*, y^*, z^*) & \approx \sum_{i=1}^7 c_i u_{z,i} + \end{align} + +The interpolation can be expressed similar to that in equation :eq:`operators_interpolation_2d_sys`, +however: + +.. math:: + \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} + \;\;\textrm{,}\;\;\;\; + \boldsymbol{u^*} = \begin{bmatrix} \boldsymbol{u_x^*} \\ \boldsymbol{u_y^*} \\ \boldsymbol{u_z^*} \end{bmatrix} + \;\;\;\;\textrm{and}\;\;\;\; + \boldsymbol{P} = \begin{bmatrix} \boldsymbol{P_x} & \boldsymbol{0} & \boldsymbol{0} \\ + \boldsymbol{0} & \boldsymbol{P_y} & \boldsymbol{0} \\ + \boldsymbol{0} & \boldsymbol{0} & \boldsymbol{P_z} + \end{bmatrix} + diff --git a/docs/images/approximate_derivative.PNG b/docs/images/approximate_derivative.PNG new file mode 100644 index 000000000..9f4e5ee61 Binary files /dev/null and b/docs/images/approximate_derivative.PNG differ diff --git a/docs/images/averaging_1d.PNG b/docs/images/averaging_1d.PNG new file mode 100644 index 000000000..415df4c3b Binary files /dev/null and b/docs/images/averaging_1d.PNG differ diff --git a/docs/images/averaging_2d.PNG b/docs/images/averaging_2d.PNG new file mode 100644 index 000000000..1563d942b Binary files /dev/null and b/docs/images/averaging_2d.PNG differ diff --git a/docs/images/averaging_2d_edges.PNG b/docs/images/averaging_2d_edges.PNG new file mode 100644 index 000000000..754810d92 Binary files /dev/null and b/docs/images/averaging_2d_edges.PNG differ diff --git a/docs/images/averaging_2d_faces.PNG b/docs/images/averaging_2d_faces.PNG new file mode 100644 index 000000000..c953da05d Binary files /dev/null and b/docs/images/averaging_2d_faces.PNG differ diff --git a/docs/images/boundary_conditions_curl.png b/docs/images/boundary_conditions_curl.png new file mode 100644 index 000000000..1aa44bdd9 Binary files /dev/null and b/docs/images/boundary_conditions_curl.png differ diff --git a/docs/images/boundary_conditions_divergence.PNG b/docs/images/boundary_conditions_divergence.PNG new file mode 100644 index 000000000..f8a5cdea4 Binary files /dev/null and b/docs/images/boundary_conditions_divergence.PNG differ diff --git a/docs/images/boundary_conditions_gradient.PNG b/docs/images/boundary_conditions_gradient.PNG new file mode 100644 index 000000000..4142fd38b Binary files /dev/null and b/docs/images/boundary_conditions_gradient.PNG differ diff --git a/docs/images/cell_locations.png b/docs/images/cell_locations.png new file mode 100644 index 000000000..b92a53566 Binary files /dev/null and b/docs/images/cell_locations.png differ diff --git a/docs/images/center_discretization.PNG b/docs/images/center_discretization.PNG new file mode 100644 index 000000000..2844822ee Binary files /dev/null and b/docs/images/center_discretization.PNG differ diff --git a/docs/images/curl_discretization.PNG b/docs/images/curl_discretization.PNG new file mode 100644 index 000000000..c79f52893 Binary files /dev/null and b/docs/images/curl_discretization.PNG differ diff --git a/docs/images/divergence_discretization.PNG b/docs/images/divergence_discretization.PNG new file mode 100644 index 000000000..ca637c155 Binary files /dev/null and b/docs/images/divergence_discretization.PNG differ diff --git a/docs/images/edge_discretization.PNG b/docs/images/edge_discretization.PNG new file mode 100644 index 000000000..a3234d907 Binary files /dev/null and b/docs/images/edge_discretization.PNG differ diff --git a/docs/images/face_discretization.PNG b/docs/images/face_discretization.PNG new file mode 100644 index 000000000..361eaea20 Binary files /dev/null and b/docs/images/face_discretization.PNG differ diff --git a/docs/images/finitevolumeschematic.png b/docs/images/finitevolumeschematic.png new file mode 100644 index 000000000..73e35fccf Binary files /dev/null and b/docs/images/finitevolumeschematic.png differ diff --git a/docs/images/gradient_discretization.PNG b/docs/images/gradient_discretization.PNG new file mode 100644 index 000000000..3db878cd6 Binary files /dev/null and b/docs/images/gradient_discretization.PNG differ diff --git a/docs/images/interpolation_1d.PNG b/docs/images/interpolation_1d.PNG new file mode 100644 index 000000000..4f4d8ada1 Binary files /dev/null and b/docs/images/interpolation_1d.PNG differ diff --git a/docs/images/interpolation_1d_sparse.PNG b/docs/images/interpolation_1d_sparse.PNG new file mode 100644 index 000000000..0cff85519 Binary files /dev/null and b/docs/images/interpolation_1d_sparse.PNG differ diff --git a/docs/images/interpolation_2d.PNG b/docs/images/interpolation_2d.PNG new file mode 100644 index 000000000..35574a426 Binary files /dev/null and b/docs/images/interpolation_2d.PNG differ diff --git a/docs/images/interpolation_2d_vectors.PNG b/docs/images/interpolation_2d_vectors.PNG new file mode 100644 index 000000000..6fad3c0f9 Binary files /dev/null and b/docs/images/interpolation_2d_vectors.PNG differ diff --git a/docs/images/mesh_types.png b/docs/images/mesh_types.png new file mode 100644 index 000000000..4e3e65281 Binary files /dev/null and b/docs/images/mesh_types.png differ diff --git a/docs/images/node_discretization.PNG b/docs/images/node_discretization.PNG new file mode 100644 index 000000000..f8c6365f8 Binary files /dev/null and b/docs/images/node_discretization.PNG differ diff --git a/docs/index.rst b/docs/index.rst index f813feb31..ec42fc5c1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,14 +26,18 @@ :caption: Reference documentation api/index.rst + content/references .. toctree:: :maxdepth: 2 :hidden: :caption: Theory - content/finite_volume - content/inner_products + content/finite_volume_index + content/theory/meshes_index + content/theory/operators_index + content/theory/inner_products_index + content/theory/derivation_examples_index .. toctree:: :maxdepth: 2 diff --git a/examples/plot_cahn_hilliard.py b/examples/plot_cahn_hilliard.py index b8354ab54..ba013a8c3 100644 --- a/examples/plot_cahn_hilliard.py +++ b/examples/plot_cahn_hilliard.py @@ -6,32 +6,30 @@ Please see their documentation for more information about the Cahn-Hilliard equation. -The "Cahn-Hilliard" equation separates a field \\\\( \\\\phi \\\\) +The "Cahn-Hilliard" equation separates a field :math:`\\phi` into 0 and 1 with smooth transitions. .. math:: - \\frac{\partial \phi}{\partial t} = \\nabla \cdot D \\nabla \left( \\frac{\partial f}{\partial \phi} - \epsilon^2 \\nabla^2 \phi \\right) + \\frac{\\partial \\phi}{\\partial t} = \\nabla \\cdot D \\nabla \\left( \\frac{\\partial f}{\\partial \\phi} - \\epsilon^2 \\nabla^2 \\phi \\right) -Where \\\\( f \\\\) is the energy function \\\\( f = ( a^2 / 2 )\\\\phi^2(1 - \\\\phi)^2 \\\\) -which drives \\\\( \\\\phi \\\\) towards either 0 or 1, this competes with the term -\\\\(\\\\epsilon^2 \\\\nabla^2 \\\\phi \\\\) which is a diffusion term that creates smooth changes in \\\\( \\\\phi \\\\). +Where :math:`f` is the energy function :math:`f = ( a^2 / 2 )\\phi^2 (1 - \\phi)^2` +which drives :math:`\\phi` towards either 0 or 1. This competes with the term +:math:`\\epsilon^2 \\nabla^2 \\phi` which is a diffusion term that creates smooth changes in :math:`\\phi`. The equation can be factored: .. math:: + \\frac{\\partial\\phi}{\partial t} = \\nabla \cdot D \\nabla \psi \n + \\psi = \\frac{\\partial^2 f}{\\partial \\phi^2} (\\phi - \\phi^{\\text{old}}) + \\frac{\\partial f}{\\partial \\phi} - \\epsilon^2 \\nabla^2 \\phi - \\frac{\partial \phi}{\partial t} = \\nabla \cdot D \\nabla \psi \\\\ - \psi = \\frac{\partial^2 f}{\partial \phi^2} (\phi - \phi^{\\text{old}}) + \\frac{\partial f}{\partial \phi} - \epsilon^2 \\nabla^2 \phi - -Here we will need the derivatives of \\\\( f \\\\): +Here we will need the derivatives of :math:`f`: .. math:: - - \\frac{\partial f}{\partial \phi} = (a^2/2)2\phi(1-\phi)(1-2\phi) - \\frac{\partial^2 f}{\partial \phi^2} = (a^2/2)2[1-6\phi(1-\phi)] + \\frac{\\partial f}{\\partial \\phi} = a^2 \\phi(1-\\phi)(1-2\\phi) \n + \\frac{\\partial^2 f}{\\partial \\phi^2} = a^2 [1-6\\phi(1-\\phi)] The implementation below uses backwards Euler in time with an -exponentially increasing time step. The initial \\\\( \\\\phi \\\\) +exponentially increasing time step. The initial :math:`\\phi` is a normally distributed field with a standard deviation of 0.1 and mean of 0.5. The grid is 60x60 and takes a few seconds to solve ~130 times. The results are seen below, and you can see the field separating diff --git a/requirements_dev.txt b/requirements_dev.txt index c3e8adb2b..14a88b5e4 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,6 +3,7 @@ sphinx sphinx_rtd_theme sphinx-gallery sphinxcontrib-apidoc +sphinxcontrib-bibtex pillow pytest pytest-cov diff --git a/tutorials/inner_products/1_1_basic_scalar.py b/tutorials/inner_products/1_1_basic_scalar.py new file mode 100644 index 000000000..06200b9e0 --- /dev/null +++ b/tutorials/inner_products/1_1_basic_scalar.py @@ -0,0 +1,167 @@ +r""" +Basic Scalar Inner Products +=========================== + +The inner product between two scalar quantities represents the most +basic class of inner products. For this class of inner products, we demonstrate: + + - How to construct the inner product matrix for scalar on nodes or at cell centers + - How to use inner product matricies to approximate the inner product + - How to construct the inverse of the inner product matrix + +""" + +############################################################################ +# Background Theory +# ----------------- +# +# For scalar quantities :math:`\psi` and :math:`\phi`, the +# inner product is defined as: +# +# .. math:: +# (\psi , \phi ) = \int_\Omega \psi \, \phi \, dv +# +# In discretized form, we can approximate the aforementioned inner-products as: +# +# .. math:: +# (\psi , \phi) \approx \mathbf{\psi^T \, M \, \phi} +# +# where :math:`\mathbf{M}` represents the *inner-product matrix*. +# :math:`\mathbf{\psi}` and :math:`\mathbf{\phi}` +# are discrete variables that live on the mesh (nodes or cell centers). +# +# It is important to note a few things about the inner-product matrix: +# +# 1. It depends on the dimensions and discretization of the mesh +# 2. It depends on whether the discrete scalar quantities live at cell centers or on nodes +# +# For this simple class of inner products, the inner product matricies for +# discrete scalar quantities living on various parts of the mesh have the form: +# +# .. math:: +# \textrm{Centers:} \; \mathbf{M_c} &= \textrm{diag} (\mathbf{v} ) \\ +# \textrm{Nodes:} \; \mathbf{M_n} &= \frac{1}{2^{2k}} \mathbf{P_n^T } \textrm{diag} (\mathbf{v} ) \mathbf{P_n} +# where +# +# - :math:`\mathbf{v}` is a vector that contains the cell volumes +# - :math:`k = 1,2,3` is the dimension (1D, 2D or 3D) +# - :math:`\mathbf{P_n}` is a projection matrix that maps from nodes to cell centers +# +# + +#################################################### +# +# Import Packages +# --------------- +# + +from discretize.utils import sdiag +from discretize import TensorMesh +import matplotlib.pyplot as plt +import numpy as np + +# sphinx_gallery_thumbnail_number = 1 + + +##################################################### +# Inner Product Matrices in 1D +# ---------------------------- +# +# Here we define a scalar function (a Gaussian distribution): +# +# .. math:: +# \phi(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \, e^{- \frac{(x- \mu )^2}{2 \sigma^2}} +# +# +# We then use the inner product matrx to approximate the following inner product +# numerically: +# +# .. math:: +# (\phi , \phi) = \int_\Omega \phi^2 \, dx = \frac{1}{2\sigma \sqrt{\pi}} +# +# +# To evaluate our approximation, we compare agains the analytic solution. +# *Note that the method for evaluating inner products here can be +# extended to variables in 2D and 3D*. +# + +# Define the Gaussian function +def fcn_gaussian(x, mu, sig): + return (1 / np.sqrt(2 * np.pi * sig ** 2)) * np.exp(-0.5 * (x - mu) ** 2 / sig ** 2) + + +# Create a tensor mesh that is sufficiently large +h = 0.1 * np.ones(100) +mesh = TensorMesh([h], "C") + +# Define center point and standard deviation +mu = 0 +sig = 1.5 + +# Evaluate at cell centers and nodes +phi_c = fcn_gaussian(mesh.gridCC, mu, sig) +phi_n = fcn_gaussian(mesh.gridN, mu, sig) + +# Define inner-product matricies +Mc = sdiag(mesh.vol) # cell-centered +# Mn = mesh.getNodalInnerProduct() # on nodes (*functionality pending*) + +# Compute the inner product +ipt = 1 / (2 * sig * np.sqrt(np.pi)) # true value of (phi, phi) +ipc = np.dot(phi_c, (Mc * phi_c)) # inner product for cell centers +# ipn = np.dot(phi_n, (Mn*phi_n)) (*functionality pending*) + +fig = plt.figure(figsize=(3, 3)) +ax = fig.add_subplot(111) +ax.plot(mesh.cell_centers, phi_c) +ax.set_title("phi at cell centers") + +# Verify accuracy +print("ACCURACY") +print("Analytic solution: ", ipt) +print("Cell-centered approx.:", ipc) +# print('Nodal approx.: ', ipn) + + + +############################################## +# Inverse of Inner Product Matricies +# ---------------------------------- +# +# The final discretized system using the finite volume method may contain +# the inverse of an inner-product matrix. Here we show how the inverse of +# the inner product matrix can be explicitly constructed. We then validate its +# accuracy for cell-centers and nodes by computing the following +# L2-norm for each: +# +# .. math:: +# \| \mathbf{u - M^{-1} M u} \|^2 +# +# which we expect to be very small. +# + + +# Create a tensor mesh +h = 0.1 * np.ones(100) +mesh = TensorMesh([h, h], "CC") + +# Inner product and inverse for cell centered scalar quantities +Mc = sdiag(mesh.vol) +Mc_inv = sdiag(1 / mesh.vol) + +# Inner product and inverse for nodal scalar quantities +# Mn = mesh.get_nodal_inner_product() +# Mn_inv = mesh.get_nodal_inner_product(invert_matrix=True) + +# Generate a random vector +phi_c = np.random.rand(mesh.nC) +phi_n = np.random.rand(mesh.nN) + +# Compute the norm +norm_c = np.linalg.norm(phi_c - Mc_inv.dot(Mc.dot(phi_c))) +# norm_n = np.linalg.norm(phi_n - Mn_inv.dot(Mn.dot(phi_n))) + +# Verify accuracy +print("ACCURACY") +print("Norm for centers:", norm_c) +print("Norm for nodes:", norm_c) diff --git a/tutorials/inner_products/1_2_basic_vector.py b/tutorials/inner_products/1_2_basic_vector.py new file mode 100644 index 000000000..2ce519a23 --- /dev/null +++ b/tutorials/inner_products/1_2_basic_vector.py @@ -0,0 +1,192 @@ +r""" +Basic Vector Inner Products +=========================== + +The inner product between two vector quantities represents one of the most +basic classes of inner products. For this class of inner products, we demonstrate: + + - How to construct the inner product matrix + - How to use inner product matricies to approximate the inner product + - How to construct the inverse of the inner product matrix. + +""" + +############################################################################ +# Background Theory +# ----------------- +# +# For vector quantities :math:`\vec{u}` and :math:`\vec{w}`, the +# inner product is given by: +# +# .. math:: +# (\vec{u}, \vec{w}) = \int_\Omega \vec{u} \cdot \vec{w} \, dv +# +# In discretized form, we can approximate the aforementioned inner-products as: +# +# .. math:: +# (\vec{u}, \vec{w}) \approx \boldsymbol{u^T M \, w} +# +# where :math:`\mathbf{M}` represents the *inner-product matrix*. +# :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` +# are discrete variables that live on the mesh (face or edges). +# +# It is important to note a few things about the inner-product matrix: +# +# 1. It depends on the dimensions and discretization of the mesh +# 2. It depends on whether the discrete scalar quantities live on faces or edges +# +# For this simple class of inner products, the corresponding form of the inner product matrices for +# discrete vector quantities living on faces and edges are shown below: +# +# .. math:: +# \textrm{Vectors on faces:} \; \boldsymbol{M_f} &= \frac{1}{4} \boldsymbol{P_f^T } \textrm{diag} (\boldsymbol{e_k \otimes v} ) \boldsymbol{P_f} \\ +# \textrm{Vectors on edges:} \; \boldsymbol{M_e} &= \frac{1}{4^{k-1}} \boldsymbol{P_e^T } \textrm{diag} (\boldsymbol{e_k \otimes v}) \boldsymbol{P_e} +# +# where +# +# - :math:`\boldsymbol{v}` is a vector that stores all of the volumes of the cells +# - :math:`k = 1,2,3` represent the dimension (1D, 2D or 3D) +# - :math:`\boldsymbol{e_k}` is a vector of 1s of length :math:`k` +# - :math:`\otimes` is the kronecker product +# - :math:`\boldsymbol{P_f}` is a projection matrix that maps quantities from faces to cell centers +# - :math:`\boldsymbol{P_e}` is a projection matrix that maps quantities from faces to cell centers +# +# + + +#################################################### +# +# Import Packages +# --------------- +# + +from discretize.utils import sdiag +from discretize import TensorMesh +import matplotlib.pyplot as plt +import numpy as np + +# sphinx_gallery_thumbnail_number = 1 + + +##################################################### +# Approximating the Inner Product for 2D Vectors +# ---------------------------------------------- +# +# To preserve the natural boundary conditions for each cell, it is standard +# practice to define fields on cell edges and fluxes on cell faces. Here we +# will define a 2D vector quantity: +# +# .. math:: +# \vec{u}(x,y) = \Bigg [ \frac{-y}{r} \hat{x} + \frac{x}{r} \hat{y} \Bigg ] +# \, e^{-\frac{x^2+y^2}{2\sigma^2}} +# +# We will then evaluate the following inner product: +# +# .. math:: +# (\vec{u}, \vec{u}) = \int_\Omega \vec{u} \cdot \vec{u} \, da +# = 2 \pi \sigma^2 +# +# using inner-product matricies. Next we compare the numerical evaluation +# of the inner products with the analytic solution. *Note that the method for +# evaluating inner products here can be extended to variables in 3D*. +# + +# Define vector components of the function +def fcn_x(xy, sig): + return (-xy[:, 1] / np.sqrt(np.sum(xy ** 2, axis=1))) * np.exp( + -0.5 * np.sum(xy ** 2, axis=1) / sig ** 2 + ) + +def fcn_y(xy, sig): + return (xy[:, 0] / np.sqrt(np.sum(xy ** 2, axis=1))) * np.exp( + -0.5 * np.sum(xy ** 2, axis=1) / sig ** 2 + ) + +# The analytic solution of (u, u) +sig = 1.5 +ipt = np.pi * sig ** 2 + +# Create a tensor mesh that is sufficiently large +h = 0.1 * np.ones(100) +mesh = TensorMesh([h, h], "CC") + +# Evaluate inner-product using edge-defined discrete variables +ux = fcn_x(mesh.edges_x, sig) +uy = fcn_y(mesh.edges_y, sig) +u = np.r_[ux, uy] + +Me = mesh.get_edge_inner_product() # Edge inner product matrix + +ipe = np.dot(u, Me * u) + +# Evaluate inner-product using face-defined discrete variables +ux = fcn_x(mesh.faces_x, sig) +uy = fcn_y(mesh.faces_y, sig) +u = np.r_[ux, uy] + +Mf = mesh.get_face_inner_product() # Edge inner product matrix + +ipf = np.dot(u, Mf * u) + + +# Plot the vector function +fig = plt.figure(figsize=(5, 5)) +ax = fig.add_subplot(111) +mesh.plot_image( + u, ax=ax, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} +) +ax.set_title("u at cell faces") + +fig.show() + +# Verify accuracy +print("ACCURACY") +print("Analytic solution: ", ipt) +print("Edge variable approx.:", ipe) +print("Face variable approx.:", ipf) + +############################################## +# Inverse of Inner Product Matricies +# ---------------------------------- +# +# The final discretized system using the finite volume method may contain +# the inverse of an inner-product matrix. Here we show how the inverse of +# the inner product matrix can be explicitly constructed. We validate its +# accuracy for edges and faces by computing the folling +# L2-norm for each: +# +# .. math:: +# \| \mathbf{u - M^{-1} M u} \|^2 +# +# which we expect to be small +# + + +# Create a tensor mesh +h = 0.1 * np.ones(100) +mesh = TensorMesh([h, h], "CC") + +# Cell centered for scalar quantities +Mc = sdiag(mesh.vol) +Mc_inv = sdiag(1 / mesh.vol) + +# Inner product for edges +Me = mesh.get_edge_inner_product() +Me_inv = mesh.get_edge_inner_product(invert_matrix=True) + +# Inner product for faces +Mf = mesh.get_face_inner_product() +Mf_inv = mesh.get_face_inner_product(invert_matrix=True) + +# Generate some random vectors +vec_e = np.random.rand(mesh.nE) +vec_f = np.random.rand(mesh.nF) + +# Compute norms +norm_e = np.linalg.norm(vec_e - Me_inv * Me * vec_e) +norm_f = np.linalg.norm(vec_f - Mf_inv * Mf * vec_f) + +# Verify accuracy +print("ACCURACY") +print("Norm for edges: ", norm_e) +print("Norm for faces: ", norm_f) diff --git a/tutorials/inner_products/1_basic.py b/tutorials/inner_products/1_basic.py deleted file mode 100644 index 426290631..000000000 --- a/tutorials/inner_products/1_basic.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -Basic Inner Products -==================== - -Inner products between two scalar or vector quantities represents the most -basic class of inner products. For this class of inner products, we demonstrate: - - - How to construct the inner product matrix - - How to use inner product matricies to approximate the inner product - - How to construct the inverse of the inner product matrix. - -For scalar quantities :math:`\\psi` and :math:`\\phi`, the -inner product is given by: - -.. math:: - (\\psi , \\phi ) = \\int_\\Omega \\psi \\, \\phi \\, dv - - -And for vector quantities :math:`\\vec{u}` and :math:`\\vec{v}`, the -inner product is given by: - -.. math:: - (\\vec{u}, \\vec{v}) = \\int_\\Omega \\vec{u} \\cdot \\vec{v} \\, dv - - -In discretized form, we can approximate the aforementioned inner-products as: - -.. math:: - (\\psi , \\phi) \\approx \\mathbf{\\psi^T \\, M \\, \\phi} - - -and - -.. math:: - (\\vec{u}, \\vec{v}) \\approx \\mathbf{u^T \\, M \\, v} - - -where :math:`\\mathbf{M}` in either equation represents an -*inner-product matrix*. :math:`\\mathbf{\\psi}`, :math:`\\mathbf{\\phi}`, -:math:`\\mathbf{u}` and :math:`\\mathbf{v}` are discrete variables that live -on the mesh. It is important to note a few things about the -inner-product matrix in this case: - - 1. It depends on the dimensions and discretization of the mesh - 2. It depends on where the discrete variables live; e.g. edges, faces, nodes, centers - -For this simple class of inner products, the inner product matricies for -discrete quantities living on various parts of the mesh have the form: - -.. math:: - \\textrm{Centers:} \\; \\mathbf{M_c} &= \\textrm{diag} (\\mathbf{v} ) \n - \\textrm{Nodes:} \\; \\mathbf{M_n} &= \\frac{1}{2^{2k}} \\mathbf{P_n^T } \\textrm{diag} (\\mathbf{v} ) \\mathbf{P_n} \n - \\textrm{Faces:} \\; \\mathbf{M_f} &= \\frac{1}{4} \\mathbf{P_f^T } \\textrm{diag} (\\mathbf{I_k \\otimes v} ) \\mathbf{P_f} \n - \\textrm{Edges:} \\; \\mathbf{M_e} &= \\frac{1}{4^{k-1}} \\mathbf{P_e^T } \\textrm{diag} (\\mathbf{I_k \\otimes v}) \\mathbf{P_e} - -where :math:`k = 1,2,3`, :math:`\\mathbf{I_k}` is the identity matrix and -:math:`\\otimes` is the kronecker product. :math:`\\mathbf{P}` are projection -matricies that map quantities from one part of the cell (nodes, faces, edges) -to cell centers. - - -""" - -#################################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial -# - -from discretize.utils import sdiag -from discretize import TensorMesh -import matplotlib.pyplot as plt -import numpy as np - -# sphinx_gallery_thumbnail_number = 2 - - -##################################################### -# Scalars -# ------- -# -# It is natural for scalar quantities to live at cell centers or nodes. Here -# we will define a scalar function (a Gaussian distribution in this case): -# -# .. math:: -# \phi(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \, e^{- \frac{(x- \mu )^2}{2 \sigma^2}} -# -# -# We will then evaluate the following inner product: -# -# .. math:: -# (\phi , \phi) = \int_\Omega \phi^2 \, dx = \frac{1}{2\sigma \sqrt{\pi}} -# -# -# according to the mid-point rule using inner-product matricies. Next we -# compare the numerical approximation of the inner product with the analytic -# solution. *Note that the method for evaluating inner products here can be -# extended to variables in 2D and 3D*. -# - - -# Define the Gaussian function -def fcn_gaussian(x, mu, sig): - - return (1 / np.sqrt(2 * np.pi * sig ** 2)) * np.exp(-0.5 * (x - mu) ** 2 / sig ** 2) - - -# Create a tensor mesh that is sufficiently large -h = 0.1 * np.ones(100) -mesh = TensorMesh([h], "C") - -# Define center point and standard deviation -mu = 0 -sig = 1.5 - -# Evaluate at cell centers and nodes -phi_c = fcn_gaussian(mesh.gridCC, mu, sig) -phi_n = fcn_gaussian(mesh.gridN, mu, sig) - -# Define inner-product matricies -Mc = sdiag(mesh.vol) # cell-centered -# Mn = mesh.getNodalInnerProduct() # on nodes (*functionality pending*) - -# Compute the inner product -ipt = 1 / (2 * sig * np.sqrt(np.pi)) # true value of (f, f) -ipc = np.dot(phi_c, (Mc * phi_c)) -# ipn = np.dot(phi_n, (Mn*phi_n)) (*functionality pending*) - -fig = plt.figure(figsize=(5, 5)) -ax = fig.add_subplot(111) -ax.plot(mesh.gridCC, phi_c) -ax.set_title("phi at cell centers") - -# Verify accuracy -print("ACCURACY") -print("Analytic solution: ", ipt) -print("Cell-centered approx.:", ipc) -# print('Nodal approx.: ', ipn) - - -##################################################### -# Vectors -# ------- -# -# To preserve the natural boundary conditions for each cell, it is standard -# practice to define fields on cell edges and fluxes on cell faces. Here we -# will define a 2D vector quantity: -# -# .. math:: -# \vec{v}(x,y) = \Bigg [ \frac{-y}{r} \hat{x} + \frac{x}{r} \hat{y} \Bigg ] -# \, e^{-\frac{x^2+y^2}{2\sigma^2}} -# -# We will then evaluate the following inner product: -# -# .. math:: -# (\vec{v}, \vec{v}) = \int_\Omega \vec{v} \cdot \vec{v} \, da -# = 2 \pi \sigma^2 -# -# using inner-product matricies. Next we compare the numerical evaluation -# of the inner products with the analytic solution. *Note that the method for -# evaluating inner products here can be extended to variables in 3D*. -# - - -# Define components of the function -def fcn_x(xy, sig): - return (-xy[:, 1] / np.sqrt(np.sum(xy ** 2, axis=1))) * np.exp( - -0.5 * np.sum(xy ** 2, axis=1) / sig ** 2 - ) - - -def fcn_y(xy, sig): - return (xy[:, 0] / np.sqrt(np.sum(xy ** 2, axis=1))) * np.exp( - -0.5 * np.sum(xy ** 2, axis=1) / sig ** 2 - ) - - -# Create a tensor mesh that is sufficiently large -h = 0.1 * np.ones(100) -mesh = TensorMesh([h, h], "CC") - -# Define center point and standard deviation -sig = 1.5 - -# Evaluate inner-product using edge-defined discrete variables -vx = fcn_x(mesh.gridEx, sig) -vy = fcn_y(mesh.gridEy, sig) -v = np.r_[vx, vy] - -Me = mesh.getEdgeInnerProduct() # Edge inner product matrix - -ipe = np.dot(v, Me * v) - -# Evaluate inner-product using face-defined discrete variables -vx = fcn_x(mesh.gridFx, sig) -vy = fcn_y(mesh.gridFy, sig) -v = np.r_[vx, vy] - -Mf = mesh.getFaceInnerProduct() # Edge inner product matrix - -ipf = np.dot(v, Mf * v) - -# The analytic solution of (v, v) -ipt = np.pi * sig ** 2 - -# Plot the vector function -fig = plt.figure(figsize=(5, 5)) -ax = fig.add_subplot(111) -mesh.plotImage( - v, ax=ax, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} -) -ax.set_title("v at cell faces") - -fig.show() - -# Verify accuracy -print("ACCURACY") -print("Analytic solution: ", ipt) -print("Edge variable approx.:", ipe) -print("Face variable approx.:", ipf) - -############################################## -# Inverse of Inner Product Matricies -# ---------------------------------- -# -# The final discretized system using the finite volume method may contain -# the inverse of an inner-product matrix. Here we show how the inverse of -# the inner product matrix can be explicitly constructed. We validate its -# accuracy for cell-centers, nodes, edges and faces by computing the folling -# L2-norm for each: -# -# .. math:: -# \| \mathbf{v - M^{-1} M v} \|^2 -# -# - - -# Create a tensor mesh -h = 0.1 * np.ones(100) -mesh = TensorMesh([h, h], "CC") - -# Cell centered for scalar quantities -Mc = sdiag(mesh.vol) -Mc_inv = sdiag(1 / mesh.vol) - -# Nodes for scalar quantities (*functionality pending*) -# Mn = mesh.getNodalInnerProduct() -# Mn_inv = mesh.getNodalInnerProduct(invMat=True) - -# Edges for vector quantities -Me = mesh.getEdgeInnerProduct() -Me_inv = mesh.getEdgeInnerProduct(invMat=True) - -# Faces for vector quantities -Mf = mesh.getFaceInnerProduct() -Mf_inv = mesh.getFaceInnerProduct(invMat=True) - -# Generate some random vectors -phi_c = np.random.rand(mesh.nC) -phi_n = np.random.rand(mesh.nN) -vec_e = np.random.rand(mesh.nE) -vec_f = np.random.rand(mesh.nF) - -# Generate some random vectors -norm_c = np.linalg.norm(phi_c - Mc_inv.dot(Mc.dot(phi_c))) -# norm_n = np.linalg.norm(phi_n - Mn_inv*Mn*phi_n) -norm_e = np.linalg.norm(vec_e - Me_inv * Me * vec_e) -norm_f = np.linalg.norm(vec_f - Mf_inv * Mf * vec_f) - -# Verify accuracy -print("ACCURACY") -print("Norm for centers:", norm_c) -# print('Norm for nodes: ', norm_n) -print("Norm for edges: ", norm_e) -print("Norm for faces: ", norm_f) diff --git a/tutorials/inner_products/2_1_constitutive_isotropic.py b/tutorials/inner_products/2_1_constitutive_isotropic.py new file mode 100644 index 000000000..a58e27022 --- /dev/null +++ b/tutorials/inner_products/2_1_constitutive_isotropic.py @@ -0,0 +1,187 @@ +r""" +Constitutive Relations (Isotropic) +================================== + +A constitutive relationship quantifies the response of a material to an external stimulus. +Examples include Ohm's law and Hooke's law. +When the constitutive relationship is isotropic, the relationship +is between quantities is may be defined by a constant. + +When solving PDEs using the finite volume approach, inner products may +contain constitutive relations. In this tutorial, you will learn how to: + + - construct the inner-product matrix in the case of isotropic constitutive relations + - construct the inverse of the inner-product matrix + - work with constitutive relations defined by the reciprocal of a parameter + +""" + +##################################################### +# Background Theory +# ----------------- +# +# Let :math:`\vec{v}` and :math:`\vec{w}` be two physically related +# quantities. If their relationship is isotropic (defined by a constant +# :math:`\sigma`), then the constitutive relation is given by: +# +# .. math:: +# \vec{v} = \sigma \vec{w} +# +# The inner product between a vector :math:`\vec{u}` and the right-hand side +# of this expression is given by: +# +# .. math:: +# (\vec{u}, \sigma \vec{w} ) = \int_\Omega \vec{v} \cdot \sigma \vec{w} \, dv +# +# Assuming the constitutive relationship is spatially invariant within each +# cell of a mesh, the inner product can be approximated numerically using +# an *inner-product matrix* such that: +# +# .. math:: +# (\vec{u}, \sigma \vec{w} ) \approx \mathbf{u^T M w} +# +# where the inner product matrix :math:`\mathbf{M}` depends on: +# +# 1. the dimensions and discretization of the mesh +# 2. where discrete variables :math:`\mathbf{u}` and :math:`\mathbf{w}` live +# 3. the spatial distribution of the property :math:`\sigma` +# +# + +#################################################### +# Import Packages +# --------------- +# + +from discretize import TensorMesh +import numpy as np +import matplotlib.pyplot as plt + +# sphinx_gallery_thumbnail_number = 1 + +##################################################### +# Inner Product for a Single Cell in 2D and 3D +# -------------------------------------------- +# +# Here we construct the inner product matrices for a single cell in 2D and in 3D. +# When approximating the inner product according to the finite volume approach, +# the constitutive parameters are defined at cell centers; even if the +# fields/fluxes live at cell edges/faces. +# + +# Create a single 3D cell +h = np.ones(1) +mesh_2d = TensorMesh([h, h]) +mesh_3d = TensorMesh([h, h, h]) + +# Constant defining the constitutive relation for the cell +sig = 10 +sig = np.array([sig]) + +# Inner products for a single 2D cell +Mf_2d = mesh_2d.get_face_inner_product(sig) # Faces inner product matrix +Me_2d = mesh_2d.get_edge_inner_product(sig) # Edges inner product matrix + +# Inner products for a single 2D cell +Mf_3d = mesh_3d.get_face_inner_product(sig) # Faces inner product matrix +Me_3d = mesh_3d.get_edge_inner_product(sig) # Edges inner product matrix + +# Plotting matrix entries +fig = plt.figure(figsize=(9, 9)) + +ax11 = fig.add_subplot(221) +ax11.imshow(Mf_2d.todense()) +ax11.set_title("Faces inner product (2D)") + +ax12 = fig.add_subplot(222) +ax12.imshow(Me_2d.todense()) +ax12.set_title("Edges inner product (2D)") + +ax21 = fig.add_subplot(223) +ax21.imshow(Mf_3d.todense()) +ax21.set_title("Faces inner product (3D)") + +ax22 = fig.add_subplot(224) +ax22.imshow(Me_3d.todense()) +ax22.set_title("Edges inner product (3D)") + +############################################################# +# Spatially Variant Parameters +# ---------------------------- +# +# In practice, the parameter :math:`\sigma` will vary spatially. +# In this case, we define the parameter :math:`\sigma` for each cell. +# When creating the inner product matrix, we enter these parameters as +# a numpy array. This is demonstrated below. Properties of the resulting +# inner product matrices are discussed. +# + +# Create a small 3D mesh +h = np.ones(5) +mesh = TensorMesh([h, h, h]) + +# Define contitutive relation for each cell +sig = np.random.rand(mesh.nC) + +# Define inner product matrices +Me = mesh.get_edge_inner_product(sig) # Edges inner product matrix +Mf = mesh.get_face_inner_product(sig) # Faces inner product matrix + +# Properties of inner product matricies +print("\n EDGE INNER PRODUCT MATRIX") +print("- Number of edges :", mesh.nE) +print("- Dimensions of operator :", str(mesh.nE), "x", str(mesh.nE)) +print("- Number non-zero (isotropic) :", str(Me.nnz), "\n") + +print("\n FACE INNER PRODUCT MATRIX") +print("- Number of faces :", mesh.nF) +print("- Dimensions of operator :", str(mesh.nF), "x", str(mesh.nF)) +print("- Number non-zero (isotropic) :", str(Mf.nnz), "\n") + + +############################################################# +# Inverse of the Inner Product Matrix +# ----------------------------------- +# +# You may need to compute the inverse of the inner product matrix for +# constitutive relationships. Here we show how to call this +# using the *invert_matrix* keyword argument. +# For the isotropic case, the inner product matrix is diagonal. +# As a result, its inverse can be easily formed. +# +# We validate the accuracy for the inverse of the inner product matrix +# for edges and faces by computing the following L2-norm for each: +# +# .. math:: +# \| \mathbf{u - M^{-1} M u} \|^2 +# +# which we expect to be small. +# + +# Create a small 3D mesh +h = np.ones(5) +mesh = TensorMesh([h, h, h]) + +# Define the constitutive relationship for each cell +sig = np.random.rand(mesh.nC) + +# Inner product and inverse at edges +Me = mesh.get_edge_inner_product(sig) +Me_inv = mesh.get_edge_inner_product(sig, invert_matrix=True) + +# Inner product and inverse at faces +Mf = mesh.get_face_inner_product(sig) +Mf_inv = mesh.get_face_inner_product(sig, invert_matrix=True) + +# Generate some random vectors +vec_e = np.random.rand(mesh.nE) +vec_f = np.random.rand(mesh.nF) + +# Compute norms +norm_e = np.linalg.norm(vec_e - Me_inv * Me * vec_e) +norm_f = np.linalg.norm(vec_f - Mf_inv * Mf * vec_f) + +# Verify accuracy +print("ACCURACY") +print("Norm for edges: ", norm_e) +print("Norm for faces: ", norm_f) diff --git a/tutorials/inner_products/2_physical_properties.py b/tutorials/inner_products/2_2_constitutive_anisotropic.py similarity index 60% rename from tutorials/inner_products/2_physical_properties.py rename to tutorials/inner_products/2_2_constitutive_anisotropic.py index 0b5d9e71e..88c558374 100644 --- a/tutorials/inner_products/2_physical_properties.py +++ b/tutorials/inner_products/2_2_constitutive_anisotropic.py @@ -1,77 +1,65 @@ -""" -Constitutive Relations -====================== - -When solving PDEs using the finite volume approach, inner products may -contain constitutive relations; examples include Ohm's law and Hooke's law. -For this class of inner products, you will learn how to: - - - Construct the inner-product matrix in the case of isotropic and anisotropic constitutive relations - - Construct the inverse of the inner-product matrix - - Work with constitutive relations defined by the reciprocal of a parameter - -Let :math:`\\vec{J}` and :math:`\\vec{E}` be two physically related -quantities. If their relationship is isotropic (defined by a constant -:math:`\\sigma`), then the constitutive relation is given by: - -.. math:: - \\vec{J} = \\sigma \\vec{E} - -The inner product between a vector :math:`\\vec{v}` and the right-hand side -of this expression is given by: - -.. math:: - (\\vec{v}, \\sigma \\vec{E} ) = \\int_\\Omega \\vec{v} \\cdot \\sigma \\vec{E} \\, dv - -Just like in the previous tutorial, we would like to approximate the inner -product numerically using an *inner-product matrix* such that: - -.. math:: - (\\vec{v}, \\sigma \\vec{E} ) \\approx \\mathbf{v^T M_\\sigma e} - -where the inner product matrix :math:`\\mathbf{M_\\sigma}` now depends on: - - 1. the dimensions and discretization of the mesh - 2. where :math:`\\mathbf{v}` and :math:`\\mathbf{e}` live - 3. the spatial distribution of the property :math:`\\sigma` - -In the case of anisotropy, the constitutive relations are defined by a tensor -(:math:`\\Sigma`). Here, the constitutive relation is of the form: +r""" +Constitutive Relations (Anisotropic) +==================================== -.. math:: - \\vec{J} = \\Sigma \\vec{E} +A constitutive relationship quantifies the response of a material to an external stimulus. +Examples include Ohm's law and Hooke's law. +When the constitutive relationship is anisotropic, the relationship +is between quantities is direction-dependent and must be defined by a tensor. -where - -.. math:: - \\Sigma = \\begin{bmatrix} \\sigma_{1} & \\sigma_{4} & \\sigma_{5} \n - \\sigma_{4} & \\sigma_{2} & \\sigma_{6} \n - \\sigma_{5} & \\sigma_{6} & \\sigma_{3} \\end{bmatrix} - -Is symmetric and defined by 6 independent parameters. The inner product between -a vector :math:`\\vec{v}` and the right-hand side of this expression is given -by: - -.. math:: - (\\vec{v}, \\Sigma \\vec{E} ) = \\int_\\Omega \\vec{v} \\cdot \\Sigma \\vec{E} \\, dv +When solving PDEs using the finite volume approach, inner products may +contain anisotropic constitutive relations. In this tutorial, you will learn how to: -Once again we would like to approximate the inner product numerically using an -*inner-product matrix* :math:`\\mathbf{M_\\Sigma}` such that: + - construct the inner-product matrix in the case of anisotropic constitutive relations + - construct the inverse of the inner-product matrix + - work with constitutive relations defined by the reciprocal of a parameter -.. math:: - (\\vec{v}, \\Sigma \\vec{E} ) \\approx \\mathbf{v^T M_\\Sigma e} - +""" +##################################################### +# Background Theory +# ----------------- +# +# Let :math:`\vec{v}` and :math:`\vec{w}` be two physically related +# quantities. If their relationship is anisotropic, +# then the constitutive relation is given by: +# +# .. math:: +# \vec{v} = \Sigma \vec{w} +# +# where +# +# .. math:: +# \Sigma = \begin{bmatrix} \sigma_{1} & \sigma_{4} & \sigma_{5} \\ +# \sigma_{4} & \sigma_{2} & \sigma_{6} \\ +# \sigma_{5} & \sigma_{6} & \sigma_{3} \end{bmatrix} +# +# is symmetric and defined by 6 independent parameters. The inner product between +# a vector :math:`\vec{u}` and the right-hand side of this expression is given +# by: +# +# .. math:: +# (\vec{u}, \Sigma \vec{w} ) = \int_\Omega \vec{u} \cdot \Sigma \vec{w} \, dv +# +# Assuming the constitutive relationship is spatially invariant within each +# cell of a mesh, the inner product can be approximated numerically using +# an *inner-product matrix* such that: +# +# .. math:: +# (\vec{u}, \Sigma \vec{w} ) \approx \mathbf{u^T M w} +# +# where the inner product matrix :math:`\mathbf{M}` depends on: +# +# 1. the dimensions and discretization of the mesh +# 2. where discrete variables :math:`\mathbf{u}` and :math:`\mathbf{w}` live +# 3. the spatial distribution and properties of the tensor :math:`\Sigma` +# -""" #################################################### -# # Import Packages # --------------- # -# Here we import the packages required for this tutorial -# from discretize import TensorMesh import numpy as np @@ -108,14 +96,14 @@ # Isotropic case sig = sig1 * np.ones((1, 1)) sig_tensor_1 = np.diag(sig1 * np.ones(3)) -Me1 = mesh.getEdgeInnerProduct(sig) # Edges inner product matrix -Mf1 = mesh.getFaceInnerProduct(sig) # Faces inner product matrix +Me1 = mesh.get_edge_inner_product(sig) # Edges inner product matrix +Mf1 = mesh.get_face_inner_product(sig) # Faces inner product matrix # Diagonal anisotropic sig = np.c_[sig1, sig2, sig3] sig_tensor_2 = np.diag(np.array([sig1, sig2, sig3])) -Me2 = mesh.getEdgeInnerProduct(sig) -Mf2 = mesh.getFaceInnerProduct(sig) +Me2 = mesh.get_edge_inner_product(sig) +Mf2 = mesh.get_face_inner_product(sig) # Full anisotropic sig = np.c_[sig1, sig2, sig3, sig4, sig5, sig6] @@ -123,8 +111,8 @@ sig_tensor_3[(0, 1), (1, 0)] = sig4 sig_tensor_3[(0, 2), (2, 0)] = sig5 sig_tensor_3[(1, 2), (2, 1)] = sig6 -Me3 = mesh.getEdgeInnerProduct(sig) -Mf3 = mesh.getFaceInnerProduct(sig) +Me3 = mesh.get_edge_inner_product(sig) +Mf3 = mesh.get_face_inner_product(sig) # Plotting matrix entries fig = plt.figure(figsize=(12, 12)) @@ -170,9 +158,9 @@ # Spatially Variant Parameters # ---------------------------- # -# In practice, the parameter :math:`\sigma` or tensor :math:`\Sigma` will -# vary spatially. In this case, we define the parameter -# :math:`\sigma` (or parameters :math:`\Sigma`) for each cell. When +# In practice, the tensor :math:`\Sigma` will +# vary spatially. In this case, we must define the +# 6 parameters in :math:`\Sigma` for each cell. When # creating the inner product matrix, we enter these parameters as # a numpy array. This is demonstrated below. Properties of the resulting # inner product matricies are discussed. @@ -183,21 +171,28 @@ mesh = TensorMesh([h, h, h]) # Isotropic case: (nC, ) numpy array -sig = np.random.rand(mesh.nC) # sig for each cell -Me1 = mesh.getEdgeInnerProduct(sig) # Edges inner product matrix -Mf1 = mesh.getFaceInnerProduct(sig) # Faces inner product matrix +sig = np.random.rand(mesh.nC) # sig for each cell +Me1 = mesh.get_edge_inner_product(sig) # Edges inner product matrix +Mf1 = mesh.get_face_inner_product(sig) # Faces inner product matrix # Linear case: (nC, dim) numpy array sig = np.random.rand(mesh.nC, mesh.dim) -Me2 = mesh.getEdgeInnerProduct(sig) -Mf2 = mesh.getFaceInnerProduct(sig) +Me2 = mesh.get_edge_inner_product(sig) +Mf2 = mesh.get_face_inner_product(sig) # Anisotropic case: (nC, 3) for 2D and (nC, 6) for 3D sig = np.random.rand(mesh.nC, 6) -Me3 = mesh.getEdgeInnerProduct(sig) -Mf3 = mesh.getFaceInnerProduct(sig) +Me3 = mesh.get_edge_inner_product(sig) +Mf3 = mesh.get_face_inner_product(sig) # Properties of inner product matricies +print("\n EDGE INNER PRODUCT MATRIX") +print("- Number of edges :", mesh.nE) +print("- Dimensions of operator :", str(mesh.nE), "x", str(mesh.nE)) +print("- Number non-zero (isotropic) :", str(Me1.nnz)) +print("- Number non-zero (linear) :", str(Me2.nnz)) +print("- Number non-zero (anisotropic):", str(Me3.nnz), "\n") + print("\n FACE INNER PRODUCT MATRIX") print("- Number of faces :", mesh.nF) print("- Dimensions of operator :", str(mesh.nF), "x", str(mesh.nF)) @@ -205,21 +200,14 @@ print("- Number non-zero (linear) :", str(Mf2.nnz)) print("- Number non-zero (anisotropic):", str(Mf3.nnz), "\n") -print("\n EDGE INNER PRODUCT MATRIX") -print("- Number of faces :", mesh.nE) -print("- Dimensions of operator :", str(mesh.nE), "x", str(mesh.nE)) -print("- Number non-zero (isotropic) :", str(Me1.nnz)) -print("- Number non-zero (linear) :", str(Me2.nnz)) -print("- Number non-zero (anisotropic):", str(Me3.nnz), "\n") - ############################################################# # Inverse # ------- # -# The final discretized system using the finite volume method may contain -# the inverse of the inner-product matrix. Here we show how to call this -# using the *invMat* keyword argument. +# You may need to compute the inverse of the inner product matrix for +# constitutive relationships. Here we show how to call this +# using the *invert_matrix* keyword argument. # # For the isotropic and diagonally anisotropic cases, the inner product matrix # is diagonal. As a result, its inverse can be easily formed. For the full @@ -238,18 +226,18 @@ # Isotropic case: (nC, ) numpy array sig = np.random.rand(mesh.nC) -Me1_inv = mesh.getEdgeInnerProduct(sig, invMat=True) -Mf1_inv = mesh.getFaceInnerProduct(sig, invMat=True) +Me1_inv = mesh.get_edge_inner_product(sig, invert_matrix=True) +Mf1_inv = mesh.get_face_inner_product(sig, invert_matrix=True) # Diagonal anisotropic: (nC, dim) numpy array sig = np.random.rand(mesh.nC, mesh.dim) -Me2_inv = mesh.getEdgeInnerProduct(sig, invMat=True) -Mf2_inv = mesh.getFaceInnerProduct(sig, invMat=True) +Me2_inv = mesh.get_edge_inner_product(sig, invert_matrix=True) +Mf2_inv = mesh.get_face_inner_product(sig, invert_matrix=True) # Full anisotropic: (nC, 3) for 2D and (nC, 6) for 3D sig = np.random.rand(mesh.nC, 6) -Me3 = mesh.getEdgeInnerProduct(sig) -Mf3 = mesh.getFaceInnerProduct(sig) +Me3 = mesh.get_edge_inner_product(sig) +Mf3 = mesh.get_face_inner_product(sig) ########################################################################### @@ -258,32 +246,15 @@ # # At times, the constitutive relation may be defined by the reciprocal of # a parameter (:math:`\rho`). Here we demonstrate how inner product matricies -# can be formed using the keyword argument *invProp*. We will do this for a +# can be formed using the keyword argument *invert_model*. We will do this for a # single cell and plot the matrix elements. We can easily extend this to # a mesh comprised of many cells. # -# In this case, the constitutive relation is given by: -# -# .. math:: -# \vec{J} = \frac{1}{\rho} \vec{E} -# -# The inner product between a vector :math:`\\vec{v}` and the right-hand side -# of the expression is given by: -# -# .. math:: -# (\vec{v}, \rho^{-1} \vec{E} ) = \int_\Omega \vec{v} \cdot \rho^{-1} \vec{E} \, dv -# -# where the inner product is approximated using an inner product matrix -# :math:`\mathbf{M_{\rho^{-1}}}` as follows: -# -# .. math:: -# (\vec{v}, \rho^{-1} \vec{E} ) \approx \mathbf{v^T M_{\rho^{-1}} e} -# # In the case that the constitutive relation is defined by a # tensor :math:`P`, e.g.: # # .. math:: -# \vec{J} = P \vec{E} +# \vec{v} = P \vec{w} # # where # @@ -292,17 +263,17 @@ # \rho_{4}^{-1} & \rho_{2}^{-1} & \rho_{6}^{-1} \\ # \rho_{5}^{-1} & \rho_{6}^{-1} & \rho_{3}^{-1} \end{bmatrix} # -# The inner product between a vector :math:`\vec{v}` and the right-hand side of +# The inner product between a vector :math:`\vec{u}` and the right-hand side of # this expression is given by: # # .. math:: -# (\vec{v}, P \vec{E} ) = \int_\Omega \vec{v} \cdot P \vec{E} \, dv +# (\vec{u}, P \vec{w} ) = \int_\Omega \vec{u} \cdot P \vec{w} \, dv # # Once again we would like to approximate the inner product numerically using an # *inner-product matrix* :math:`\mathbf{M_P}` such that: # # .. math:: -# (\vec{v}, P \vec{E} ) \approx \mathbf{v^T M_P e} +# (\vec{u}, P \vec{w} ) \approx \mathbf{u^T M_P w} # # Here we demonstrate how to form the inner-product matricies # :math:`\mathbf{M_{\rho^{-1}}}` and :math:`\mathbf{M_P}`. @@ -324,18 +295,18 @@ # Isotropic case rho = rho1 * np.ones((1, 1)) -Me1 = mesh.getEdgeInnerProduct(rho, invProp=True) # Edges inner product matrix -Mf1 = mesh.getFaceInnerProduct(rho, invProp=True) # Faces inner product matrix +Me1 = mesh.get_edge_inner_product(rho, inverse_model=True) # Edges inner product matrix +Mf1 = mesh.get_face_inner_product(rho, inverse_model=True) # Faces inner product matrix # Diagonal anisotropic case rho = np.c_[rho1, rho2, rho3] -Me2 = mesh.getEdgeInnerProduct(rho, invProp=True) -Mf2 = mesh.getFaceInnerProduct(rho, invProp=True) +Me2 = mesh.get_edge_inner_product(rho, inverse_model=True) +Mf2 = mesh.get_face_inner_product(rho, inverse_model=True) # Full anisotropic case rho = np.c_[rho1, rho2, rho3, rho4, rho5, rho6] -Me3 = mesh.getEdgeInnerProduct(rho, invProp=True) -Mf3 = mesh.getFaceInnerProduct(rho, invProp=True) +Me3 = mesh.get_edge_inner_product(rho, inverse_model=True) +Mf3 = mesh.get_face_inner_product(rho, inverse_model=True) # Plotting matrix entries fig = plt.figure(figsize=(14, 9)) diff --git a/tutorials/inner_products/3_1_gradient.py b/tutorials/inner_products/3_1_gradient.py new file mode 100644 index 000000000..13ebb7ad9 --- /dev/null +++ b/tutorials/inner_products/3_1_gradient.py @@ -0,0 +1,102 @@ +r""" +Gradient Operator +================= + +When solving PDEs using the finite volume approach, inner products may +contain the gradient operator. Where :math:`\phi` is a scalar quantity +and :math:`\vec{u}` is a vector quantity, we may need to derive a +discrete approximation to the following inner product: + +.. math:: + (\vec{u} , \nabla \phi) = \int_\Omega \, \vec{u} \cdot \nabla \phi \, dv + +In this section, we demonstrate how to go from the inner product to the +discrete approximation. In doing so, we must construct +discrete differential operators, inner product matricies and consider +boundary conditions. + + +""" + +#################################################### +# Background Theory +# ----------------- +# +# For the inner product between a vector :math:`\vec{u}` and +# the gradient of a scalar :math:`\phi`, +# there are two options for where the variables should live. +# +# **For** :math:`\boldsymbol{\phi}` **on the nodes and** :math:`\boldsymbol{u}` **on cell edges:** +# +# .. math:: +# \int_\Omega \vec{u} \cdot \nabla \phi \, dv \approx \boldsymbol{u^T M_e G \, \phi} +# +# where +# +# - :math:`\boldsymbol{M_e}` is the basic inner product matrix for vectors at edges +# - :math:`\boldsymbol{G}` is the discrete gradient operator which maps from nodes to edges +# +# **For** :math:`\boldsymbol{\phi}` **at cell centers and** :math:`\boldsymbol{u}` **on cell faces**, +# the gradient operator would have to map from cell centers to faces. This would require knowledge +# of :math:`\phi` outside the domain for boundary faces. In this case, we use the identity +# :math:`\vec{u} \cdot \nabla \phi = \nabla \cdot \phi\vec{u} - \phi \nabla \cdot \vec{u}` +# and apply the divergence theorem such that: +# +# .. math:: +# \int_\Omega \vec{u} \cdot \nabla \phi \, dv = +# - \int_\Omega \phi \nabla \cdot \vec{u} \, dv + \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da +# \approx - \boldsymbol{u^T D^T M_c \, \phi} + \boldsymbol{u^T B \, \phi} +# = \boldsymbol{u^T \tilde{G} \, \phi} +# +# where +# +# - :math:`\boldsymbol{D}` is the discrete divergence operator from faces to cell centers +# - :math:`\boldsymbol{M_c}` is the basic inner product matrix for scalars at cell centers +# - :math:`\boldsymbol{B}` is a sparse matrix that imposes the boundary conditions on :math:`\phi` +# - :math:`\boldsymbol{\tilde{G}} = \boldsymbol{-D^T M_c + B}` acts as a modified gradient operator with boundary conditions imposed +# +# Note that when :math:`\phi = 0` on the boundary, the term containing :math:`\boldsymbol{B}` is zero. +# + + +#################################################### +# Import Packages +# --------------- +# + +from discretize.utils import sdiag +from discretize import TensorMesh +import numpy as np +import matplotlib.pyplot as plt + + +##################################################### +# Gradient +# -------- +# + +# Make basic mesh +h = np.ones(10) +mesh = TensorMesh([h, h, h]) + +# Items required to perform u.T*(Me*Gn*phi) +Me = mesh.getEdgeInnerProduct() # Basic inner product matrix (edges) +Gn = mesh.nodalGrad # Nodes to edges gradient + +# Items required to perform u.T*(Mf*Gc*phi) +Mf = mesh.getFaceInnerProduct() # Basic inner product matrix (faces) +mesh.setCellGradBC(["neumann", "dirichlet", "neumann"]) # Set boundary conditions +Gc = mesh.cellGrad # Cells to faces gradient + +# Plot Sparse Representation +fig = plt.figure(figsize=(5, 6)) + +ax1 = fig.add_subplot(121) +ax1.spy(Me * Gn, markersize=0.5) +ax1.set_title("Me*Gn") + +ax2 = fig.add_subplot(122) +ax2.spy(Mf * Gc, markersize=0.5) +ax2.set_title("Mf*Gc") + + diff --git a/tutorials/inner_products/3_2_divergence.py b/tutorials/inner_products/3_2_divergence.py new file mode 100644 index 000000000..8c2c1d294 --- /dev/null +++ b/tutorials/inner_products/3_2_divergence.py @@ -0,0 +1,89 @@ +r""" +Divergence Operator +=================== + +When solving PDEs using the finite volume approach, inner products may +contain the divergence operator. Where :math:`\psi` is a scalar quantity +and :math:`\vec{w}` is a vector quantity, we may need to derive a +discrete approximation to the following inner product: + +.. math:: + (\psi , \nabla \cdot \vec{w}) = \int_\Omega \, \psi \, \nabla \cdot \vec{w} \, dv + +In this section, we demonstrate how to go from the inner product to the +discrete approximation. In doing so, we must construct +discrete differential operators, inner product matricies and consider +boundary conditions. + + +""" + +#################################################### +# Import Packages +# --------------- +# + +from discretize.utils import sdiag +from discretize import TensorMesh +import numpy as np +import matplotlib.pyplot as plt + +#################################################### +# Background Theory +# ----------------- +# +# For the inner product between a scalar (:math:`\psi`) and the divergence of a vector (:math:`\vec{w}`), +# there are two options for where the variables should live. +# +# **For** :math:`\boldsymbol{\psi}` **at cell centers and** :math:`\boldsymbol{w}` **on cell faces:** +# +# .. math:: +# \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv \approx \boldsymbol{\psi^T M_c D \, w} +# +# where +# +# - :math:`\boldsymbol{D}` is the discrete divergence operator from faces to cell centers +# - :math:`\boldsymbol{M_c}` is the basic inner product matrix for scalars at cell centers +# +# **For** :math:`\boldsymbol{\psi}` **on the nodes and** :math:`\boldsymbol{w}` **on cell edges** , +# the divergence operator would have to map from edges to nodes. For boundary nodes, this would +# require knowledge of :math:`\vec{w}` outside the boundary. To evaluate the inner product we use the identity +# :math:`\psi \nabla \cdot \vec{w} = \nabla \cdot \psi\vec{w} - \vec{w} \cdot \nabla \psi` +# and apply the divergence theorem such that: +# +# .. math:: +# \int_\Omega \psi \; (\nabla \cdot \vec{w}) \, dv = +# - \int_\Omega \vec{w} \cdot \nabla \psi \, dv + \oint_{\partial \Omega} \psi (\hat{n} \cdot \vec{w}) \, da +# \approx - \boldsymbol{\psi^T G^T M_e \, w } + \boldsymbol{\psi^T B \, w } +# = \boldsymbol{\psi^T \tilde{D} \, w } +# +# where +# +# - :math:`\boldsymbol{G}` is the discrete gradient operator from nodes to edges +# - :math:`\boldsymbol{M_e}` is the basic inner product matrix for vectors on edges +# - :math:`\boldsymbol{B}` is a sparse matrix that imposes boundary conditions on :math:`\vec{w}` +# - :math:`\boldsymbol{\tilde{D}} = \boldsymbol{- G^T M_e + B}` acts as a modified divergence operator with boundary conditions imposed +# +# Note that when :math:`\hat{n} \cdot \vec{w} = 0` on the boundary, the term containing :math:`\boldsymbol{B}` is zero. +# + + +##################################################### +# Divergence +# ---------- +# + +# Make basic mesh +h = np.ones(10) +mesh = TensorMesh([h, h, h]) + +# Items required to perform psi.T*(Mc*D*v) +Mc = sdiag(mesh.vol) # Basic inner product matrix (centers) +D = mesh.faceDiv # Faces to centers divergence + +# Plot sparse representation +fig = plt.figure(figsize=(8, 5)) + +ax1 = fig.add_subplot(111) +ax1.spy(Mc * D, markersize=0.5) +ax1.set_title("Mc*D", pad=20) diff --git a/tutorials/inner_products/3_3_curl.py b/tutorials/inner_products/3_3_curl.py new file mode 100644 index 000000000..0ba65092c --- /dev/null +++ b/tutorials/inner_products/3_3_curl.py @@ -0,0 +1,100 @@ +r""" +Curl Operator +============= + +When solving PDEs using the finite volume approach, inner products may +contain the curl operator. Where :math:`\vec{u}` and :math:`\vec{w}` are vector +quantities, we may need to derive a discrete approximation for the following +inner product: + +.. math:: + (\vec{u} , \nabla \times \vec{w}) = \int_\Omega \, \vec{u} \cdot \vec{w} \, dv + +In this section, we demonstrate how to go from the inner product to the +discrete approximation. In doing so, we must construct +discrete differential operators, inner product matricies and consider +boundary conditions. + + +""" + +#################################################### +# Background Theory +# ----------------- +# +# For the inner product between a vector (:math:`\vec{u}`) and the +# curl of another vector (:math:`\vec{w}`), +# there are two options for where the variables should live. +# +# **For** :math:`\boldsymbol{u}` **on the faces and** :math:`\boldsymbol{w}` **on the edges:** +# +# .. math:: +# \int_\Omega \vec{u} \cdot (\nabla \times \vec{w} ) \, dv \approx \boldsymbol{u^T M_f C \, w} +# +# where +# +# - :math:`\boldsymbol{C}` is the discrete curl operator from edges to faces +# - :math:`\boldsymbol{M_f}` is the basic inner product matrix for vectors on cell faces +# +# **For** :math:`\boldsymbol{u}` **on the edges and** :math:`\boldsymbol{w}` **on cell faces** , +# the curl would need to map from faces to edges. In this case, it is better to use the identity +# :math:`\vec{u} \cdot (\nabla \times \vec{w}) = \vec{w} \cdot (\nabla \times \vec{u}) - \nabla \cdot (\vec{u} \times \vec{w})` +# and to apply the divergence theorem such that: +# +# .. math:: +# \int_\Omega \vec{u} \cdot (\nabla \times \vec{w} ) \, dv +# = \int_\Omega \vec{w} \cdot (\nabla \times \vec{u} ) \, dv - \oint_{\partial \Omega} (\vec{u} \times \vec{w}) \cdot d\vec{a} +# \approx \boldsymbol{u^T C^T \! M_f \, w } + \boldsymbol{u^T B \, w } +# = \boldsymbol{u^T \tilde{C} \, w } +# +# where +# +# - :math:`\boldsymbol{C}` is still the discrete curl operator from edges to faces +# - :math:`\boldsymbol{M_f}` is still the basic inner product matrix for vectors on cell faces +# - :math:`\boldsymbol{B}` is a sparse matrix which imposes boundary conditions on :math:`\vec{w}` +# - :math:`\boldsymbol{\tilde{C}} = \boldsymbol{C^T \! M_f + B}` acts as a modified curl operator with boundary conditions imposed +# +# +# Note that :math:`\boldsymbol{u^T B \, w }=0` when :math:`\hat{n} \times \vec{w} = 0` on the boundary. +# + +#################################################### +# Import Packages +# --------------- +# + +from discretize.utils import sdiag +from discretize import TensorMesh +import numpy as np +import matplotlib.pyplot as plt + + +##################################################### +# Curl +# ---- +# + + +# Make basic mesh +h = np.ones(10) +mesh = TensorMesh([h, h, h]) + +# Items required to perform u.T*(Mf*Ce*v) +Mf = mesh.getFaceInnerProduct() # Basic inner product matrix (faces) +Ce = mesh.edgeCurl # Edges to faces curl + +# Items required to perform u.T*(Me*Cf*v) +Me = mesh.getEdgeInnerProduct() # Basic inner product matrix (edges) +Cf = mesh.edgeCurl.T # Faces to edges curl (assumes Dirichlet) + +# Plot Sparse Representation +fig = plt.figure(figsize=(9, 5)) + +ax1 = fig.add_subplot(121) +ax1.spy(Mf * Ce, markersize=0.5) +ax1.set_title("Mf*Ce", pad=10) + +ax2 = fig.add_subplot(122) +ax2.spy(Me * Cf, markersize=0.5) +ax2.set_title("Me*Cf", pad=10) + diff --git a/tutorials/inner_products/3_calculus.py b/tutorials/inner_products/3_calculus.py deleted file mode 100644 index 81fda6834..000000000 --- a/tutorials/inner_products/3_calculus.py +++ /dev/null @@ -1,265 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Differential Operators -====================== - -When solving PDEs using the finite volume approach, inner products may -contain differential operators. Where :math:`\\psi` and :math:`\\phi` are -scalar quantities, and :math:`\\vec{u}` and :math:`\\vec{v}` are vector -quantities, we may need to derive a discrete approximation for the following -inner products: - - 1. :math:`(\\vec{u} , \\nabla \\phi)` - 2. :math:`(\\psi , \\nabla \\cdot \\vec{v})` - 3. :math:`(\\vec{u} , \\nabla \\times \\vec{v})` - 4. :math:`(\\psi, \\Delta^2 \\phi)` - -In this section, we demonstrate how to go from the inner product to the -discrete approximation for each case. In doing so, we must construct -discrete differential operators, inner product matricies and consider -boundary conditions. - - - -""" - -#################################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial -# - -from discretize.utils import sdiag -from discretize import TensorMesh -import numpy as np -import matplotlib.pyplot as plt - - -##################################################### -# Gradient -# -------- -# -# Where :math:`\phi` is a scalar quantity and :math:`\vec{u}` is a vector -# quantity, we would like to evaluate the following inner product: -# -# .. math:: -# (\vec{u} , \nabla \phi) = \int_\Omega \vec{u} \cdot \nabla \phi \, dv -# -# **Inner Product at edges:** -# -# In the case that :math:`\vec{u}` represents a field, it is natural for it to -# be discretized to live on cell edges. By defining :math:`\phi` to live at -# the nodes, we can use the nodal gradient operator (:math:`\mathbf{G_n}`) to -# map from nodes to edges. The inner product is therefore computed using an -# inner product matrix (:math:`\mathbf{M_e}`) for -# quantities living on cell edges, e.g.: -# -# .. math:: -# (\vec{u} , \nabla \phi) \approx \mathbf{u^T M_e G_n \phi} -# -# **Inner Product at faces:** -# -# In the case that :math:`\vec{u}` represents a flux, it is natural for it to -# be discretized to live on cell faces. By defining :math:`\phi` to live at -# cell centers, we can use the cell gradient operator (:math:`\mathbf{G_c}`) to -# map from centers to faces. In this case, we must impose boundary conditions -# on the discrete gradient operator because it cannot use locations outside -# the mesh to evaluate the gradient on the boundary. If done correctly, the -# inner product is computed using an inner product matrix (:math:`\mathbf{M_f}`) -# for quantities living on cell faces, e.g.: -# -# .. math:: -# (\vec{u} , \nabla \phi) \approx \mathbf{u^T M_f G_c \phi} -# - -# Make basic mesh -h = np.ones(10) -mesh = TensorMesh([h, h, h]) - -# Items required to perform u.T*(Me*Gn*phi) -Me = mesh.getEdgeInnerProduct() # Basic inner product matrix (edges) -Gn = mesh.nodalGrad # Nodes to edges gradient - -# Items required to perform u.T*(Mf*Gc*phi) -Mf = mesh.getFaceInnerProduct() # Basic inner product matrix (faces) -mesh.setCellGradBC(["neumann", "dirichlet", "neumann"]) # Set boundary conditions -Gc = mesh.cellGrad # Cells to faces gradient - -# Plot Sparse Representation -fig = plt.figure(figsize=(5, 6)) - -ax1 = fig.add_subplot(121) -ax1.spy(Me * Gn, markersize=0.5) -ax1.set_title("Me*Gn") - -ax2 = fig.add_subplot(122) -ax2.spy(Mf * Gc, markersize=0.5) -ax2.set_title("Mf*Gc") - -##################################################### -# Divergence -# ---------- -# -# Where :math:`\psi` is a scalar quantity and :math:`\vec{v}` is a vector -# quantity, we would like to evaluate the following inner product: -# -# .. math:: -# (\psi , \nabla \cdot \vec{v}) = \int_\Omega \psi \nabla \cdot \vec{v} \, dv -# -# The divergence defines a measure of the flux leaving/entering a volume. As a -# result, it is natural for :math:`\vec{v}` to be a flux defined on cell faces. -# The face divergence operator (:math:`\mathbf{D}`) maps from cell faces to -# cell centers, therefore # we should define :math:`\psi` at cell centers. The -# inner product is ultimately computed using an inner product matrix -# (:math:`\mathbf{M_f}`) for quantities living on cell faces, e.g.: -# -# .. math:: -# (\psi , \nabla \cdot \vec{v}) \approx \mathbf{\psi^T} \textrm{diag} (\mathbf{vol} ) \mathbf{D v} -# - -# Make basic mesh -h = np.ones(10) -mesh = TensorMesh([h, h, h]) - -# Items required to perform psi.T*(Mc*D*v) -Mc = sdiag(mesh.vol) # Basic inner product matrix (centers) -D = mesh.faceDiv # Faces to centers divergence - -# Plot sparse representation -fig = plt.figure(figsize=(8, 5)) - -ax1 = fig.add_subplot(111) -ax1.spy(Mc * D, markersize=0.5) -ax1.set_title("Mc*D", pad=20) - -##################################################### -# Curl -# ---- -# -# Where :math:`\vec{u}` and :math:`\vec{v}` are vector quantities, we would -# like to evaluate the following inner product: -# -# .. math:: -# (\vec{u} , \nabla \times \vec{v}) = \int_\Omega \vec{u} \nabla \times \vec{v} \, dv -# -# **Inner Product at Faces:** -# -# Let :math:`\vec{u}` denote a flux and let :math:`\vec{v}` denote a field. -# In this case, it is natural for the flux :math:`\vec{u}` to live on cell -# faces and for the field :math:`\vec{v}` to live on cell edges. The discrete -# curl operator (:math:`\mathbf{C_e}`) in this case naturally maps from cell -# edges to cell faces without the need to define boundary conditions. The -# inner product can be approxiated using an inner product matrix -# (:math:`\mathbf{M_f}`) for quantities living on cell faces, e.g.: -# -# .. math:: -# (\vec{u} , \nabla \times \vec{v}) \approx \mathbf{u^T M_f C_e v} -# -# **Inner Product at Edges:** -# -# Now let :math:`\vec{u}` denote a field and let :math:`\vec{v}` denote a flux. -# Now it is natural for the :math:`\vec{u}` to live on cell edges -# and for :math:`\vec{v}` to live on cell faces. We would like to compute the -# inner product using an inner product matrix (:math:`\mathbf{M_e}`) for -# quantities living on cell edges. However, this requires a discrete curl -# operator (:math:`\mathbf{C_f}`) that maps from cell faces -# to cell edges; which requires to impose boundary conditions on the operator. -# If done successfully: -# -# .. math:: -# (\vec{u} , \nabla \times \vec{v}) \approx \mathbf{u^T M_e C_f v} -# - -# Make basic mesh -h = np.ones(10) -mesh = TensorMesh([h, h, h]) - -# Items required to perform u.T*(Mf*Ce*v) -Mf = mesh.getFaceInnerProduct() # Basic inner product matrix (faces) -Ce = mesh.edgeCurl # Edges to faces curl - -# Items required to perform u.T*(Me*Cf*v) -Me = mesh.getEdgeInnerProduct() # Basic inner product matrix (edges) -Cf = mesh.edgeCurl.T # Faces to edges curl (assumes Dirichlet) - -# Plot Sparse Representation -fig = plt.figure(figsize=(9, 5)) - -ax1 = fig.add_subplot(121) -ax1.spy(Mf * Ce, markersize=0.5) -ax1.set_title("Mf*Ce", pad=10) - -ax2 = fig.add_subplot(122) -ax2.spy(Me * Cf, markersize=0.5) -ax2.set_title("Me*Cf", pad=10) - - -########################################################### -# Scalar Laplacian -# ---------------- -# -# Where :math:`\psi` and :math:`\phi` are scalar quantities, and the scalar -# Laplacian :math:`\Delta^2 = \nabla \cdot \nabla`, we would like to -# approximate the following inner product: -# -# .. math:: -# (\psi , \nabla \cdot \nabla \phi) = \int_\Omega \psi (\nabla \cdot \nabla \phi) \, dv -# -# Using :math:`p \nabla \cdot \mathbf{q} = \nabla \cdot (p \mathbf{q}) - \mathbf{q} \cdot (\nabla p )` -# and the Divergence theorem we obtain: -# -# .. math:: -# \int_{\partial \Omega} \mathbf{n} \cdot ( \psi \nabla \phi ) \, da -# - \int_\Omega (\nabla \psi ) \cdot (\nabla \phi ) \, dv -# -# In this case, the surface integral can be eliminated if we can assume a -# Neumann condition of :math:`\partial \phi/\partial n = 0` on the boundary. -# -# **Inner Prodcut at Edges:** -# -# Let :math:`\psi` and :math:`\phi` be discretized to the nodes. In this case, -# the discrete gradient operator (:math:`\mathbf{G_n}`) must map from nodes -# to edges. Ultimately we evaluate the inner product using an inner product -# matrix (:math:`\mathbf{M_e}` for quantities living on cell edges, e.g.: -# -# .. math:: -# (\psi , \nabla \cdot \nabla \phi) \approx \mathbf{\psi G_n^T M_e G_n \phi} -# -# **Inner Product at Faces:** -# -# Let :math:`\psi` and :math:`\phi` be discretized to cell centers. In this -# case, the discrete gradient operator (:math:`\mathbf{G_c}`) must map from -# centers to faces; and requires the user to set Neumann conditions in the -# operator. Ultimately we evaluate the inner product using an inner product -# matrix (:math:`\mathbf{M_f}`) for quantities living on cell faces, e.g.: -# -# .. math:: -# (\psi , \nabla \cdot \nabla \phi) \approx \mathbf{\psi G_c^T M_f G_c \phi} -# -# - -# Make basic mesh -h = np.ones(10) -mesh = TensorMesh([h, h, h]) - -# Items required to perform psi.T*(Gn.T*Me*Gn*phi) -Me = mesh.getEdgeInnerProduct() # Basic inner product matrix (edges) -Gn = mesh.nodalGrad # Nodes to edges gradient - -# Items required to perform psi.T*(Gc.T*Mf*Gc*phi) -Mf = mesh.getFaceInnerProduct() # Basic inner product matrix (faces) -mesh.setCellGradBC(["dirichlet", "dirichlet", "dirichlet"]) -Gc = mesh.cellGrad # Centers to faces gradient - -# Plot Sparse Representation -fig = plt.figure(figsize=(9, 4)) - -ax1 = fig.add_subplot(121) -ax1.spy(Gn.T * Me * Gn, markersize=0.5) -ax1.set_title("Gn.T*Me*Gn", pad=5) - -ax2 = fig.add_subplot(122) -ax2.spy(Gc.T * Mf * Gc, markersize=0.5) -ax2.set_title("Gc.T*Mf*Gc", pad=5) diff --git a/tutorials/inner_products/4_advanced.py b/tutorials/inner_products/4_advanced.py deleted file mode 100644 index 400b1f97e..000000000 --- a/tutorials/inner_products/4_advanced.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -Advanced Examples -================= - -In this section, we demonstrate how to go from the inner product to the -discrete approximation for some special cases. We also show how all -necessary operators are constructed for each case. - - - -""" - -#################################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial -# - -from discretize.utils import sdiag -from discretize import TensorMesh -import numpy as np -import matplotlib.pyplot as plt - - -##################################################### -# Constitive Relations and Differential Operators -# ----------------------------------------------- -# -# Where :math:`\psi` and :math:`\phi` are scalar quantities, -# :math:`\vec{u}` and :math:`\vec{v}` are vector quantities, and -# :math:`\sigma` defines a constitutive relationship, we may need to derive -# discrete approximations for the following inner products: -# -# 1. :math:`(\vec{u} , \sigma \nabla \phi)` -# 2. :math:`(\psi , \sigma \nabla \cdot \vec{v})` -# 3. :math:`(\vec{u} , \sigma \nabla \times \vec{v})` -# -# These cases effectively combine what was learned in the previous two -# tutorials. For each case, we must: -# -# - Define discretized quantities at the appropriate mesh locations -# - Define an inner product matrix that depends on a single constitutive parameter (:math:`\sigma`) or a tensor (:math:`\Sigma`) -# - Construct differential operators that may require you to define boundary conditions -# -# Where :math:`\mathbf{M_e}(\sigma)` is the property dependent inner-product -# matrix for quantities on cell edges, :math:`\mathbf{M_f}(\sigma)` is the -# property dependent inner-product matrix for quantities on cell faces, -# :math:`\mathbf{G_{ne}}` is the nodes to edges gradient operator and -# :math:`\mathbf{G_{cf}}` is the centers to faces gradient operator: -# -# .. math:: -# (\vec{u} , \sigma \nabla \phi) &= \mathbf{u_f^T M_f}(\sigma) \mathbf{ G_{cf} \, \phi_c} \;\;\;\;\; (\vec{u} \;\textrm{on faces and} \; \phi \; \textrm{at centers}) \\ -# &= \mathbf{u_e^T M_e}(\sigma) \mathbf{ G_{ne} \, \phi_n} \;\;\;\; (\vec{u} \;\textrm{on edges and} \; \phi \; \textrm{on nodes}) -# -# Where :math:`\mathbf{M_c}(\sigma)` is the property dependent inner-product -# matrix for quantities at cell centers and :math:`\mathbf{D}` is the faces -# to centers divergence operator: -# -# .. math:: -# (\psi , \sigma \nabla \cdot \vec{v}) = \mathbf{\psi_c^T M_c} (\sigma)\mathbf{ D v_f} \;\;\;\; (\psi \;\textrm{at centers and} \; \vec{v} \; \textrm{on faces} ) -# -# Where :math:`\mathbf{C_{ef}}` is the edges to faces curl operator and -# :math:`\mathbf{C_{fe}}` is the faces to edges curl operator: -# -# .. math:: -# (\vec{u} , \sigma \nabla \times \vec{v}) &= \mathbf{u_f^T M_f} (\sigma) \mathbf{ C_{ef} \, v_e} \;\;\;\; (\vec{u} \;\textrm{on edges and} \; \vec{v} \; \textrm{on faces} )\\ -# &= \mathbf{u_e^T M_e} (\sigma) \mathbf{ C_{fe} \, v_f} \;\;\;\; (\vec{u} \;\textrm{on faces and} \; \vec{v} \; \textrm{on edges} ) -# -# **With the operators constructed below, you can compute all of the -# aforementioned inner products.** -# - - -# Make basic mesh -h = np.ones(10) -mesh = TensorMesh([h, h, h]) -sig = np.random.rand(mesh.nC) # isotropic -Sig = np.random.rand(mesh.nC, 6) # anisotropic - -# Inner product matricies -Mc = sdiag(mesh.vol * sig) # Inner product matrix (centers) -# Mn = mesh.getNodalInnerProduct(sig) # Inner product matrix (nodes) (*functionality pending*) -Me = mesh.getEdgeInnerProduct(sig) # Inner product matrix (edges) -Mf = mesh.getFaceInnerProduct(sig) # Inner product matrix for tensor (faces) - -# Differential operators -Gne = mesh.nodalGrad # Nodes to edges gradient -mesh.setCellGradBC(["neumann", "dirichlet", "neumann"]) # Set boundary conditions -Gcf = mesh.cellGrad # Cells to faces gradient -D = mesh.faceDiv # Faces to centers divergence -Cef = mesh.edgeCurl # Edges to faces curl -Cfe = mesh.edgeCurl.T # Faces to edges curl - -# EXAMPLE: (u, sig*Curl*v) -fig = plt.figure(figsize=(9, 5)) - -ax1 = fig.add_subplot(121) -ax1.spy(Mf * Cef, markersize=0.5) -ax1.set_title("Me(sig)*Cef (Isotropic)", pad=10) - -Mf_tensor = mesh.getFaceInnerProduct(Sig) # inner product matrix for tensor -ax2 = fig.add_subplot(122) -ax2.spy(Mf_tensor * Cef, markersize=0.5) -ax2.set_title("Me(sig)*Cef (Anisotropic)", pad=10) - -##################################################### -# Divergence of a Scalar and a Vector Field -# ----------------------------------------- -# -# Where :math:`\psi` and :math:`\phi` are scalar quantities, and -# :math:`\vec{u}` is a known vector field, we may need to derive -# a discrete approximation for the following inner product: -# -# .. math:: -# (\psi , \nabla \cdot \phi \vec{u}) -# -# Scalar and vector quantities are generally discretized to lie on -# different locations on the mesh. As result, it is better to use the -# identity :math:`\nabla \cdot \phi \vec{u} = \phi \nabla \cdot \vec{u} + \vec{u} \cdot \nabla \phi` -# and separate the inner product into two parts: -# -# .. math:: -# (\psi , \phi \nabla \cdot \vec{u} ) + (\psi , \vec{u} \cdot \nabla \phi) -# -# **Term 1:** -# -# If the vector field :math:`\vec{u}` is divergence free, there is no need -# to evaluate the first inner product term. This is the case for advection when -# the fluid is incompressible. -# -# Where :math:`\mathbf{D_{fc}}` is the faces to centers divergence operator, and -# :math:`\mathbf{M_c}` is the basic inner product matrix for cell centered -# quantities, we can approximate this inner product as: -# -# .. math:: -# (\psi , \phi \nabla \cdot \vec{u} ) = \mathbf{\psi_c^T M_c} \textrm{diag} (\mathbf{D_{fc} u_f} ) \, \mathbf{\phi_c} -# -# **Term 2:** -# -# Let :math:`\mathbf{G_{cf}}` be the cell centers to faces gradient operator, -# :math:`\mathbf{M_c}` be the basic inner product matrix for cell centered -# quantities, and :math:`\mathbf{\tilde{A}_{fc}}` and averages *and* sums the -# cartesian contributions of :math:`\vec{u} \cdot \nabla \phi`, we can -# approximate the inner product as: -# -# .. math:: -# (\psi , \vec{u} \cdot \nabla \phi) = \mathbf{\psi_c^T M_c \tilde A_{fc}} \text{diag} (\mathbf{u_f} ) \mathbf{G_{cf} \, \phi_c} -# -# **With the operators constructed below, you can compute all of the -# inner products.** - -# Make basic mesh -h = np.ones(10) -mesh = TensorMesh([h, h, h]) - -# Inner product matricies -Mc = sdiag(mesh.vol * sig) # Inner product matrix (centers) - -# Differential operators -mesh.setCellGradBC(["neumann", "dirichlet", "neumann"]) # Set boundary conditions -Gcf = mesh.cellGrad # Cells to faces gradient -Dfc = mesh.faceDiv # Faces to centers divergence - -# Averaging and summing matrix -Afc = mesh.dim * mesh.aveF2CC diff --git a/tutorials/inner_products/README.txt b/tutorials/inner_products/README.txt index 28d6bbc34..4a2671c55 100644 --- a/tutorials/inner_products/README.txt +++ b/tutorials/inner_products/README.txt @@ -10,5 +10,7 @@ inner products on a numerical grid is to apply the midpoint rule; which is used by the *discretize* package. Here, we demonstrate how to approximate various classes of inner -products numerically. If this is known, the user will be capable -of properly discretizing any term in a problem specific PDE. \ No newline at end of file +products numerically. The inner products can be approximated in +terms of a linear expression which contains an inner-product matrix. +By learning how to formulate each class of inner products, the user will +have the building blocks to discretize and solve a problem-specific PDE. \ No newline at end of file diff --git a/tutorials/mesh_generation/1_mesh_overview.py b/tutorials/mesh_generation/1_mesh_overview.py index e183bacfb..60b6e827c 100644 --- a/tutorials/mesh_generation/1_mesh_overview.py +++ b/tutorials/mesh_generation/1_mesh_overview.py @@ -1,100 +1,84 @@ -""" -Overview of Mesh Types -====================== +r""" +Meshes Overview +=============== -Here we provide an overview of mesh types and define some terminology. -Separate tutorials have been provided for each mesh type. +Here we provide a basic overview of the meshes supported by *discretize* +and define some useful terminology. +Detailed tutorials covering the construction and properties of each mesh type are found in +separate tutorials. -""" -import numpy as np -import discretize -import matplotlib.pyplot as plt +""" ############################################################################### -# General Categories of Meshes -# ---------------------------- -# -# The three main types of meshes in discretize are -# -# - **Tensor meshes** (:class:`discretize.TensorMesh`); which includes **cylindrical meshes** (:class:`discretize.CylMesh`) -# -# - **Tree meshes** (:class:`discretize.TreeMesh`): also referred to as QuadTree or OcTree meshes +# Meshes Supported by Discretize +# ------------------------------ +# +# A multitude of mesh types are supported by the *discretize* package. +# Each mesh type has its advantages and disadvantages when being considered to solve +# differential equations with the finite volume approach. +# The user should consider the dimension (1D, 2D, 3D) and +# any spatial symmetry in the PDE when choosing a mesh type. +# Mesh types supported by *discretize* include: +# +# - **Tensor Meshes:** A mesh where the grid locations are organized according to tensor products +# - **Tree Meshes:** A mesh where the dimensions of cells are :math:`2^n` larger than the dimension of the smallest cell size +# - **Curvilinear Meshes:** A tensor mesh where the axes are curvilinear +# - **Cylindrical Meshes:** A pseudo-2D mesh for solving 3D problems with perfect symmetry in the radial direction +# +# .. figure:: ../../images/mesh_types.png +# :align: center +# :width: 700 +# +# Examples of different mesh types supported by the *discretize* package. # -# - **Curvilinear meshes** (:class:`discretize.CurviMesh`): also referred to as logically rectangular non-orthogonal -# -# Examples for each mesh type are shown below. -# - -ncx = 16 # number of cells in the x-direction -ncy = 16 # number of cells in the y-direction - -# create a tensor mesh -tensor_mesh = discretize.TensorMesh([ncx, ncy]) - -# create a tree mesh and refine some of the cells -tree_mesh = discretize.TreeMesh([ncx, ncy]) - - -def refine(cell): - if np.sqrt(((np.r_[cell.center] - 0.5) ** 2).sum()) < 0.2: - return 4 - return 2 - - -tree_mesh.refine(refine) - -# create a curvilinear mesh -curvi_mesh = discretize.CurvilinearMesh( - discretize.utils.exampleLrmGrid([ncx, ncy], "rotate") -) - -# Plot -fig, axes = plt.subplots(1, 3, figsize=(14.5, 4)) -tensor_mesh.plotGrid(ax=axes[0]) -axes[0].set_title("TensorMesh") - -tree_mesh.plotGrid(ax=axes[1]) -axes[1].set_title("TreeMesh") - -curvi_mesh.plotGrid(ax=axes[2]) -axes[2].set_title("CurvilinearMesh") ############################################################################### -# Variable Locations and Terminology -# ---------------------------------- +# Cells and Where Variables Live +# ------------------------------ +# +# The small volumes within the mesh are called *cells*. +# In *discretize*, we use a staggered mimetic finite volume approach :cite:`haber2014,HymanShashkov1999`. +# This approach requires discrete variables to live at cell-centers, nodes, faces, or edges. +# Below, we illustrate the valid locations for discrete quantities for a single cell where: +# +# - **Centers:** center locations of each cell +# - **Nodes:** locations of intersection between grid lines defining the mesh +# - **X, Y and Z edges:** edges whose tangent lines are parallel to the X, Y and Z axes, respectively +# - **X, Y and Z faces:** faces which are normal to the orientation of the X, Y and Z axes, respectively +# +# +# .. figure:: ../../images/cell_locations.png +# :align: center +# :width: 700 +# +# Locations of centers, nodes, faces and edges for 2D cells (left) and 3D cells (right). # -# When solving differential equations on a numerical grid, variables can be -# defined on: -# -# - nodes -# - cell centers -# - cell faces -# - cell edges -# -# Below we show an example for a 2D tensor mesh. +# Below, we create a 2D tensor mesh and plot a visual representation of where discretized +# quantities are able to live. # +import numpy as np +import discretize +import matplotlib.pyplot as plt +plt.rcParams.update({'font.size':14}) + +# sphinx_gallery_thumbnail_number = 1 + hx = np.r_[3, 1, 1, 3] hy = np.r_[3, 2, 1, 1, 1, 1, 2, 3] -tensor_mesh2 = discretize.TensorMesh([hx, hy]) +tensor_mesh = discretize.TensorMesh([hx, hy]) # Plot -fig, axes2 = plt.subplots(1, 3, figsize=(14.5, 5)) -tensor_mesh2.plotGrid(ax=axes2[0], nodes=True, centers=True) -axes2[0].legend(("Nodes", "Centers")) -axes2[0].set_title("Nodes and cell centers") - -tensor_mesh2.plotGrid(ax=axes2[1], edges=True) -axes2[1].legend(("X-edges", "Y-edges")) -axes2[1].set_title("Cell edges") - -tensor_mesh2.plotGrid(ax=axes2[2], faces=True) -axes2[2].legend(("X-faces", "Y-faces")) -axes2[2].set_title("Cell faces") - -############################################################################### -# Note that we define X-edges as being edges that lie parallel to the x-axis. -# And we define X-faces as being faces whose normal lies parallel to the -# axis. In 3D, the difference between edges and faces is more obvious. -# +fig, axes = plt.subplots(1, 3, figsize=(14.5, 5)) +tensor_mesh.plot_grid(ax=axes[0], nodes=True, centers=True) +axes[0].legend(("Nodes", "Centers")) +axes[0].set_title("Nodes and cell centers") + +tensor_mesh.plot_grid(ax=axes[1], edges=True) +axes[1].legend(("X-edges", "Y-edges")) +axes[1].set_title("Cell edges") + +tensor_mesh.plot_grid(ax=axes[2], faces=True) +axes[2].legend(("X-faces", "Y-faces")) +axes[2].set_title("Cell faces") diff --git a/tutorials/mesh_generation/2_tensor_mesh.py b/tutorials/mesh_generation/2_tensor_mesh.py index 578b379d3..620aef428 100644 --- a/tutorials/mesh_generation/2_tensor_mesh.py +++ b/tutorials/mesh_generation/2_tensor_mesh.py @@ -1,15 +1,14 @@ """ -Tensor meshes +Tensor Meshes ============= -Tensor meshes are the most basic class of meshes that can be created with -discretize. They belong to the class (:class:`~discretize.TensorMesh`). +Tensor meshes are the most basic class of meshes supported by the *discretize* package. +For tensor meshes, the grid locations are organized according to tensor products. Tensor meshes can be defined in 1, 2 or 3 dimensions. Here we demonstrate: - - How to create basic tensor meshes - - How to include padding cells - - How to plot tensor meshes - - How to extract properties from meshes + - how to construct tensor meshes + - how to plot tensor meshes + - how to extract basic properties from tensor meshes """ @@ -18,8 +17,6 @@ # Import Packages # --------------- # -# Here we import the packages required for this tutorial. -# from discretize import TensorMesh import matplotlib.pyplot as plt @@ -28,12 +25,12 @@ # sphinx_gallery_thumbnail_number = 3 ############################################### -# Basic Example -# ------------- +# Basic 2D Example +# ---------------- # # The easiest way to define a tensor mesh is to define the cell widths in -# x, y and z as 1D numpy arrays. And to provide the position of the bottom -# southwest corner of the mesh. We demonstrate this here for a 2D mesh (thus +# x, y and z as 1D numpy arrays, and to provide the position of the bottom +# southwest corner of the mesh. Here we create a uniform 2D tensor mesh (thus # we do not need to consider the z-dimension). # @@ -49,7 +46,7 @@ mesh = TensorMesh([hx, hy], x0=[x0, y0]) -mesh.plotGrid() +mesh.plot_grid() ############################################### @@ -82,10 +79,10 @@ # relative to the origin mesh = TensorMesh([hx, hy], x0="CN") -# We can apply the plotGrid method and output to a specified axes object +# We can apply the plot_grid method and output to a specified axes object fig = plt.figure(figsize=(6, 6)) ax = fig.add_subplot(111) -mesh.plotGrid(ax=ax) +mesh.plot_grid(ax=ax) ax.set_xbound(mesh.x0[0], mesh.x0[0] + np.sum(mesh.hx)) ax.set_ybound(mesh.x0[1], mesh.x0[1] + np.sum(mesh.hy)) ax.set_title("Tensor Mesh") @@ -119,10 +116,10 @@ nC = mesh.nC # An (nC, 2) array containing the cell-center locations -cc = mesh.gridCC +cc = mesh.cell_centers # A boolean array specifying which cells lie on the boundary -bInd = mesh.cellBoundaryInd +bInd = mesh.cell_boundary_indices # Plot the cell areas (2D "volume") s = mesh.vol @@ -158,10 +155,10 @@ nC = mesh.nC # An (nC, 3) array containing the cell-center locations -cc = mesh.gridCC +cc = mesh.cell_centers # A boolean array specifying which cells lie on the boundary -bInd = mesh.cellBoundaryInd +bInd = mesh.cell_boundary_indices # The cell volumes v = mesh.vol diff --git a/tutorials/mesh_generation/3_cylindrical_mesh.py b/tutorials/mesh_generation/3_cylindrical_mesh.py index eac8ee133..e32f96e23 100644 --- a/tutorials/mesh_generation/3_cylindrical_mesh.py +++ b/tutorials/mesh_generation/3_cylindrical_mesh.py @@ -1,29 +1,23 @@ """ -Cylindrical meshes +Cylindrical Meshes ================== -Cylindrical meshes (:class:`~discretize.CylMesh`) are defined in terms of *r* +Cylindrical meshes are useful when solving 3D differential equations +that posess rotational symmetry. Cylindrical meshes are defined in terms of *r* (radial position), *z* (vertical position) and *phi* (azimuthal position). -They are a child class of the tensor mesh class. Cylindrical meshes are useful -in solving differential equations that possess rotational symmetry. Here we -demonstrate: - - - How to create basic cylindrical meshes - - How to include padding cells - - How to plot cylindrical meshes - - How to extract properties from meshes - - How to create cylindrical meshes to solve PDEs with rotational symmetry +Here we demonstrate: + + - how to construct cylindrical meshes + - how to plot cylindrical meshes + - how to extract properties from cylindrical meshes """ ############################################### -# # Import Packages # --------------- # -# Here we import the packages required for this tutorial. -# from discretize import CylMesh import matplotlib.pyplot as plt @@ -34,8 +28,8 @@ # ------------- # # The easiest way to define a cylindrical mesh is to define the cell widths in -# *r*, *phi* and *z* as 1D numpy arrays. And to provide a Cartesian position -# for the bottom of the vertical axis of symmetry of the mesh. Note that +# *r*, *phi* and *z* as 1D numpy arrays, and to provide a Cartesian position +# for the bottom of the vertical axis of symmetry of the mesh. Note that: # # 1. *phi* is in radians # 2. The sum of values in the numpy array for *phi* cannot exceed :math:`2\pi` @@ -58,7 +52,7 @@ mesh = CylMesh([hr, hp, hz], x0=[x0, y0, z0]) -mesh.plotGrid() +mesh.plot_grid() ############################################### @@ -92,8 +86,8 @@ # We can use flags 'C', '0' and 'N' to define the xyz position of the mesh. mesh = CylMesh([hr, hp, hz], x0="00C") -# We can apply the plotGrid method and change the axis properties -ax = mesh.plotGrid() +# We can apply the plot_grid method and change the axis properties +ax = mesh.plot_grid() ax[0].set_title("Discretization in phi") ax[1].set_title("Discretization in r and z") @@ -108,7 +102,7 @@ nC = mesh.nC # An (nC, 3) array containing the cell-center locations -cc = mesh.gridCC +cc = mesh.cell_centers # The cell volumes v = mesh.vol @@ -152,7 +146,7 @@ nC = mesh.nC # An (nC, 3) array containing the cell-center locations -cc = mesh.gridCC +cc = mesh.cell_centers # Plot the cell volumes. v = mesh.vol diff --git a/tutorials/mesh_generation/4_tree_mesh.py b/tutorials/mesh_generation/4_tree_mesh.py index a857a9de9..27ca875fc 100644 --- a/tutorials/mesh_generation/4_tree_mesh.py +++ b/tutorials/mesh_generation/4_tree_mesh.py @@ -2,35 +2,31 @@ Tree Meshes =========== -Compared to tensor meshes, tree meshes are able to provide higher levels +Tree meshes are able to provide higher levels of discretization in certain regions while reducing the total number of -cells. Tree meshes belong to the class (:class:`~discretize.TreeMesh`). -Tree meshes can be defined in 2 or 3 dimensions. Here we demonstrate: - - - How to create basic tree meshes in 2D and 3D - - Strategies for local mesh refinement - - How to plot tree meshes - - How to extract properties from tree meshes - -To create a tree mesh, we first define the base tensor mesh (a mesh -comprised entirely of the smallest cells). Next we choose the level of -discretization around certain points or within certain regions. When -creating tree meshes, we must remember certain rules: - - - The number of base mesh cells in x, y and z must all be powers of 2 - - We cannot refine the mesh to create cells smaller than those defining the base mesh - - The range of cell sizes in the tree mesh depends on the number of base mesh cells in x, y and z +cells. Tree meshes can be defined in 2 or 3 dimensions. Here we demonstrate: + + - how to create tree meshes in 2D and 3D + - strategies for local mesh refinement + - how to plot tree meshes + - how to extract properties from tree meshes + +To create a tree mesh, we first define the base tensor mesh (a uniform tensor mesh +comprised entirely of cells of the smallest size). Next we choose the level of +discretization around certain points or within certain regions. +When creating tree meshes, we must remember certain rules: + + 1. The number of base mesh cells in x, y and z must all be powers of 2 + 2. We cannot refine the mesh to create cells smaller than those defining the base mesh + 3. The range of cell sizes in the tree mesh depends on the number of base mesh cells in x, y and z """ ############################################### -# # Import Packages # --------------- # -# Here we import the packages required for this tutorial. -# from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz @@ -65,7 +61,7 @@ mesh.finalize() # Must finalize tree mesh before use -mesh.plotGrid(show_it=True) +mesh.plot_grid(show_it=True) ############################################### @@ -95,7 +91,7 @@ # Define the base mesh hx = [(dx, nbcx)] hy = [(dy, nbcy)] -mesh = TreeMesh([hx, hy], x0="CC") +mesh = TreeMesh([hx, hy], origin="CC") # Refine surface topography xx = mesh.vectorNx @@ -113,12 +109,12 @@ mesh.finalize() -# We can apply the plotGrid method and output to a specified axes object +# We can apply the plot_grid method and output to a specified axes object fig = plt.figure(figsize=(6, 6)) ax = fig.add_subplot(111) -mesh.plotGrid(ax=ax) -ax.set_xbound(mesh.x0[0], mesh.x0[0] + np.sum(mesh.hx)) -ax.set_ybound(mesh.x0[1], mesh.x0[1] + np.sum(mesh.hy)) +mesh.plot_grid(ax=ax) +ax.set_xbound(mesh.origin[0], mesh.origin[0] + np.sum(mesh.hx)) +ax.set_ybound(mesh.origin[1], mesh.origin[1] + np.sum(mesh.hy)) ax.set_title("QuadTree Mesh") #################################################### @@ -142,7 +138,7 @@ # Define the base mesh hx = [(dx, nbcx)] hy = [(dy, nbcy)] -mesh = TreeMesh([hx, hy], x0="CC") +mesh = TreeMesh([hx, hy], origin="CC") # Refine surface topography xx = mesh.vectorNx @@ -161,16 +157,16 @@ mesh.finalize() # The bottom west corner -x0 = mesh.x0 +origin = mesh.origin # The total number of cells nC = mesh.nC # An (nC, 2) array containing the cell-center locations -cc = mesh.gridCC +cc = mesh.cell_centers # A boolean array specifying which cells lie on the boundary -bInd = mesh.cellBoundaryInd +bInd = mesh.cell_boundary_indices # The cell areas (2D "volume") s = mesh.vol @@ -178,8 +174,8 @@ fig = plt.figure(figsize=(6, 6)) ax = fig.add_subplot(111) mesh.plotImage(np.log10(s), grid=True, ax=ax) -ax.set_xbound(mesh.x0[0], mesh.x0[0] + np.sum(mesh.hx)) -ax.set_ybound(mesh.x0[1], mesh.x0[1] + np.sum(mesh.hy)) +ax.set_xbound(mesh.origin[0], mesh.origin[0] + np.sum(mesh.hx)) +ax.set_ybound(mesh.origin[1], mesh.origin[1] + np.sum(mesh.hy)) ax.set_title("Log of Cell Areas") ############################################### @@ -207,7 +203,7 @@ hx = [(dx, nbcx)] hy = [(dy, nbcy)] hz = [(dz, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCC") +mesh = TreeMesh([hx, hy, hz], origin="CCC") # Refine surface topography [xx, yy] = np.meshgrid(mesh.vectorNx, mesh.vectorNy) @@ -226,16 +222,16 @@ mesh.finalize() # The bottom west corner -x0 = mesh.x0 +origin = mesh.origin # The total number of cells nC = mesh.nC # An (nC, 2) array containing the cell-center locations -cc = mesh.gridCC +cc = mesh.cell_centers # A boolean array specifying which cells lie on the boundary -bInd = mesh.cellBoundaryInd +bInd = mesh.cell_boundary_indices # Cell volumes v = mesh.vol diff --git a/tutorials/mesh_generation/README.txt b/tutorials/mesh_generation/README.txt index 92e4f3f49..883470dcf 100644 --- a/tutorials/mesh_generation/README.txt +++ b/tutorials/mesh_generation/README.txt @@ -1,19 +1,9 @@ -Mesh Generation -=============== - -`discretize` provides a numerical grid (or "mesh") on which to solve differential -equations. Each mesh type has a similar API to make working with -different meshes relatively simple. Within `discretize`, all meshes are -classes that have properties like the number of cells `nC`, and methods, -like `plotGrid`. - -To learn how to create meshes with `discretize`, we have provided a set -of tutorials. These tutorials aim to teach the user: - - - where discrete variables can live on meshes - - how to construct various types of meshes - - how to extract useful properties from mesh objects - - some aspects of plotting and meshes - - - +Meshes +====== + +A mesh is a structured numerical grid on which discrete variables are organized. +Regarding numerical solution of partial differential equations (PDEs), discrete variables +generally represent approximations to continuous functions. +Meshes can be created and used to solve PDEs in 1D, 2D or 3D. +For a given system of PDEs, the mesh defines a discrete representation of the domain and its boundaries. +Here, we provide a set of tutorials related to generating and understanding mesh types supported by the *discretize* package. diff --git a/tutorials/operators/1_1_interpolation_1d.py b/tutorials/operators/1_1_interpolation_1d.py new file mode 100644 index 000000000..56df9fbc5 --- /dev/null +++ b/tutorials/operators/1_1_interpolation_1d.py @@ -0,0 +1,217 @@ +r""" +Interpolation and Averaging in 1D +================================= + +Interpolation is used when a discrete quantity is known on the mesh (centers, nodes, edges or faces), +but we would like to estimate its value at locations within the continuous domain. +For any mesh type, *discretize* allows the user to construct a sparse interpolation matrix +for a corresponding set of locations. + +In *discretize*, averaging matrices are constructed when a discrete quantity must be mapped +between centers, nodes, edges or faces. +Averaging matrices are a property of the mesh and are constructed when called. + +In this tutorial, we demonstrate: + + - how to construct interpolation and averaging matrices + - how to apply the interpolation and averaging to 1D functions + + +""" + +############################################# +# Background Theory +# ----------------- +# +# Let us define a 1D mesh that contains 8 cells of arbitrary width. +# The mesh is illustrated in the figure below. The width of each cell is +# defined as :math:`h_i`. The location of each node is defined as :math:`x_i`. +# +# .. figure:: ../../images/interpolation_1d.png +# :align: center +# :width: 600 +# :name: operators_interpolation_1d +# +# Tensor mesh in 1D. +# +# Now let :math:`u(x)` be a function whose values are known at the nodes; +# i.e. :math:`u_i = u(x_i)`. +# The approximate value of the function at location :math:`x^*` +# using linear interpolation is given by: +# +# .. math:: +# u(x^*) \approx u_3 + \Bigg ( \frac{u_4 - u_3}{h_3} \Bigg ) (x^* - x_3) +# :label: operators_averaging_interpolation_1d +# +# +# Suppose now that we organize the known values of :math:`u(x)` at the nodes +# into a vector of the form: +# +# .. math:: +# \boldsymbol{u} = \begin{bmatrix} u_0 & u_1 & u_2 & u_3 & u_4 & u_5 & u_6 & u_7 & u_8 \end{bmatrix}^T +# +# If we define a row: +# +# .. math:: +# \boldsymbol{p_0} = \begin{bmatrix} 0 & 0 & 0 & a_3 & a_4 & 0 & 0 & 0 & 0 \end{bmatrix} +# +# where +# +# .. math:: +# a_3 = 1 - \frac{x^* - x_3}{h_3} \;\;\;\;\; \textrm{and} \;\;\;\;\; a_4 = \frac{x^* - x_3}{h_3} +# +# then +# +# .. math:: +# u(x^*) \approx \boldsymbol{p_0 \, u} +# +# For a single location, we have just seen how a linear operator can be constructed to +# compute the interpolation using a matrix vector-product. +# +# Now consider the case where you would like to interpolate the function from the nodes to +# an arbitrary number of locations within the boundaries of the mesh. +# For each location, we simply construct the corresponding row in the interpolation matrix. +# Where :math:`\boldsymbol{u^*}` is a vector containing the approximations of :math:`u(x)` at :math:`M` +# locations: +# +# .. math:: +# \boldsymbol{u^*} \approx \boldsymbol{P\, u} \;\;\;\;\;\; \textrm{where} \;\;\;\;\;\; +# \boldsymbol{P} = \begin{bmatrix} \cdots \;\; \boldsymbol{p_0} \;\; \cdots \\ +# \cdots \;\; \boldsymbol{p_1} \;\; \cdots \\ \vdots \\ +# \cdots \, \boldsymbol{p_{M-1}} \, \cdots \end{bmatrix} +# :label: operators_averaging_interpolation_matrix +# +# :math:`\boldsymbol{P}` is a sparse matrix whose rows contain a maximum of 2 non-zero elements. +# The size of :math:`\boldsymbol{P}` is the number of locations by the number of nodes. +# For seven locations (:math:`x^* = 3,1,9,2,5,2,10`) and our mesh (9 nodes), +# the non-zero elements of the interpolation matrix are illustrated below. +# +# .. figure:: ../../images/interpolation_1d_sparse.png +# :align: center +# :width: 250 +# + + +############################################### +# Import Packages +# --------------- +# + +from discretize import TensorMesh, TreeMesh +from discretize.utils import refine_tree_xyz +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +mpl.rcParams.update({'font.size':14}) + +# sphinx_gallery_thumbnail_number = 1 + + +############################################################### +# Constructing and Applying a 1D Interpolation Matrix +# --------------------------------------------------- +# +# Here we discretize a continuous scalar function to live on cell nodes, +# then interpolate the values to a set of locations within the domain. +# Next, we compute the scalar function at the specified locations to +# validate out 1D interpolation operator. +# + +# Create a uniform grid +h = 10 * np.ones(20) +mesh = TensorMesh([h], "C") + +# Define locations +x_nodes = mesh.nodes_x + +# Define a set of locations for the interpolation +np.random.seed(6) +x_interp = np.random.uniform(np.min(x_nodes), np.max(x_nodes), 20) + +# Define a continuous function +def fun(x): + return np.exp(-(x ** 2) / 50 ** 2) + +# Compute function on nodes and at the location +v_nodes = fun(x_nodes) +v_true = fun(x_interp) + +# Create interpolation matrix and apply. When creating the interpolation matrix, +# we must define where the discrete quantity lives and where it is being +# interpolated to. +P = mesh.get_interpolation_matrix(x_interp, 'N') +v_interp = P * v_nodes + +# Compare +fig = plt.figure(figsize=(12, 4)) +ax1 = fig.add_axes([0.1, 0.05, 0.25, 0.8]) +ax1.spy(P, markersize=5) +ax1.set_title("Spy plot for interpolation operator", pad=15) + +k = np.argsort(x_interp) + +ax2 = fig.add_axes([0.45, 0.1, 0.5, 0.8]) +ax2.plot( + x_nodes, v_nodes, 'k', + x_interp[k], v_true[k], "b^", + x_interp[k], v_interp[k], "gv", + x_interp[k], np.c_[v_true[k] - v_interp[k]], "ro", +) +ax2.set_ylim([-0.1, 1.5]) +ax2.set_title("Comparison plot") +ax2.legend( + ( + "original function", "true value at locations", + "interpolated from nodes", "error" + ), loc="upper right" +) + +fig.show() + + +######################################################### +# Constructing and Applying a 1D Averaging Matrix +# ----------------------------------------------- +# +# Here we compute a scalar function on cell nodes and average to cell centers. +# We then compute the scalar function at cell centers to validate the +# averaging operator. +# + +# Create a uniform grid +h = 10 * np.ones(20) +mesh = TensorMesh([h], "C") + +# Get node and cell center locations +x_nodes = mesh.vectorNx +x_centers = mesh.vectorCCx + +# Define a continuous function +def fun(x): + return np.exp(-(x ** 2) / 50 ** 2) + +# Compute function on nodes and cell centers +v_nodes = fun(x_nodes) +v_centers = fun(x_centers) + +# Create operator and average from nodes to cell centers +A = mesh.average_node_to_cell +v_approx = A * v_nodes + +# Compare +fig = plt.figure(figsize=(12, 4)) +ax1 = fig.add_axes([0.1, 0.05, 0.25, 0.8]) +ax1.spy(A, markersize=5) +ax1.set_title("Sparse representation of A", pad=10) + +ax2 = fig.add_axes([0.45, 0.1, 0.5, 0.8]) +ax2.plot( + x_centers, v_centers, "b-", + x_centers, v_approx, "ko", + x_centers, np.c_[v_centers - v_approx], "r-" +) +ax2.set_title("Comparison plot") +ax2.legend(("evaluated at centers", "averaged from nodes", "error")) + +fig.show() + diff --git a/tutorials/operators/1_2_interpolation_scalar.py b/tutorials/operators/1_2_interpolation_scalar.py new file mode 100644 index 000000000..8918e5f12 --- /dev/null +++ b/tutorials/operators/1_2_interpolation_scalar.py @@ -0,0 +1,208 @@ +r""" +Interpolating and Averaging Scalar Quantities in 2D and 3D +========================================================== + +Interpolation is used when a discrete quantity is known on the mesh (centers, nodes, edges or faces), +but we would like to estimate its value at locations within the continuous domain. +For any mesh type, *discretize* allows the user to construct a sparse interpolation matrix +for a corresponding set of locations. + +In *discretize*, averaging matrices are constructed when a discrete quantity must be mapped +between centers, nodes, edges or faces. +Averaging matrices are a property of the mesh and are constructed when called. + +In this tutorial, we demonstrate: + + - how to construct interpolation and averaging matrices + - how to apply the interpolation and averaging to scalar functions + + +""" + +############################################# +# Background Theory +# ----------------- +# +# We will begin by presenting the theory for 2D interpolation, then extend the +# theory to cover 3D interpolation. In 2D, the location of the interpolated +# quantity lies either within 4 nodes or cell centers. +# +# .. figure:: ../../images/interpolation_2d.png +# :align: center +# :width: 300 +# +# A tensor mesh in 2D denoting interpolation from nodes (blue) and cell centers (red). +# +# Let :math:`(x^*, y^*)` be within a cell whose nodes are located at +# :math:`(x_1, y_1)`, :math:`(x_2, y_1)`, :math:`(x_1, y_2)` and :math:`(x_2, y_2)`. +# If we define :math:`u_0 = u(x_1, y_1)`, :math:`u_1 = u(x_2, y_1)`, :math:`u_2 = u(x_1, y_2)` and +# :math:`u_3 = u(x_2, y_2)`, then +# +# .. math:: +# u(x^*, y^*) \approx a_0 u_0 + a_1 u_1 + a_2 u_2 + a_3 u_3 +# +# where :math:`a_0`, :math:`a_1`, :math:`a_2` and :math:`a_3` are coefficients determined from equations +# governing `bilinear interpolation `__ . +# These coefficients represent the 4 non-zero values within the corresponding row of the interpolation matrix :math:`\boldsymbol{P}`. +# +# Where the values of :math:`u(x,y)` at all nodes are organized into a single vector :math:`\boldsymbol{u}`, +# and :math:`\boldsymbol{u^*}` is a vector containing the approximations of :math:`u(x,y)` at an arbitrary number of locations: +# +# .. math:: +# \boldsymbol{u^*} \approx \boldsymbol{P\, u} +# :label: operators_interpolation_general +# +# In each row of :math:`\boldsymbol{P}`, the position of the non-zero elements :math:`a_0`, :math:`a_1`, :math:`a_2` and :math:`a_3` +# corresponds to the indecies of the 4 nodes comprising a specific cell. +# Once again the shape of :math:`\boldsymbol{P}` is the number of locations by the number of nodes. +# +# **What if the function is defined at cell centers?** +# +# A similar result can be obtained by interpolating a function define at cell centers. +# In this case, we let :math:`(x^*, y^*)` lie within 4 cell centers located at +# :math:`(\bar{x}_1, \bar{y}_1)`, :math:`(\bar{x}_2, \bar{y}_1)`, :math:`(\bar{x}_1, \bar{y}_2)` and :math:`(\bar{x}_2, \bar{y}_2)`. +# +# .. math:: +# u(x^*, y^*) \approx a_0 \bar{u}_0 + a_1 \bar{u}_1 + a_2 \bar{u}_2 + a_3 \bar{u}_3 +# +# The resulting interpolation is defined similar to expression :eq:`operators_interpolation_general`. +# However the size of the resulting interpolation matrix is the number of locations by number of cells. +# +# **What about for 3D case?** +# +# The derivation for the 3D case is effectively the same, except 8 node or center locations must +# be used in the interpolation. Thus: +# +# .. math:: +# u(x^*, y^*, z^*) \approx \sum_{k=0}^7 a_k u_k +# +# This creates an interpolation matrix :math:`\boldsymbol{P}` with 8 non-zero entries per row. +# To learn how to compute the value of the coefficients :math:`a_k`, +# see `trilinear interpolation (3D) `__ +# + + +################################################ +# Import Packages +# --------------- +# + +from discretize import TensorMesh, TreeMesh +from discretize.utils import refine_tree_xyz +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +mpl.rcParams.update({'font.size':14}) + +# sphinx_gallery_thumbnail_number = 1 + + +############################################################### +# Constructing and Applying a 2D/3D Interpolation Matrix +# ------------------------------------------------------ +# +# Here we discretize a scalar quantity to live at cell centers of a tree mesh. +# We then use the interpolation matrix to approximate the values of the +# scalar function along a profile. The approach for 2D and 3D meshes +# are essentially the same. +# + +# Construct a tree mesh +h = 2* np.ones(128) +mesh = TreeMesh([h, h], x0="CC") + +xy = np.c_[0., 0.] +mesh = refine_tree_xyz(mesh, xy, octree_levels=[8, 8, 8], method="radial", finalize=False) +mesh.finalize() # Must finalize tree mesh before use + +# Define the points along the profile +d = np.linspace(-100, 100, 21) # distance along profile +phi = 35. # heading of profile +xp = d*np.cos(np.pi*phi/180.) +yp = d*np.sin(np.pi*phi/180.) + +# Define a continuous 2D scalar function +def fun(x, y): + return np.exp(-(x ** 2 + y ** 2) / 40 ** 2) + +# Get all cell center locations from the mesh and evaluate function +# at the centers. Also compute true value at interpolation locations +centers = mesh.cell_centers +v_centers = fun(centers[:, 0], centers[:, 1]) +v_true = fun(xp, yp) + +# Create interpolation matrix and apply. When creating the interpolation matrix, +# we must define where the discrete quantity lives and where it is being +# interpolated to. +locations = np.c_[xp, yp] +P = mesh.get_interpolation_matrix(locations, 'CC') +v_interp = P * v_centers + +# Plot mesh and profile line +fig = plt.figure(figsize=(14, 4.5)) + +ax1 = fig.add_axes([0.1, 0.15, 0.25, 0.75]) +mesh.plot_grid(ax=ax1) +ax1.plot(xp, yp, 'ko') +ax1.set_xlim(np.min(mesh.nodes_x), np.max(mesh.nodes_x)) +ax1.set_ylim(np.min(mesh.nodes_y), np.max(mesh.nodes_y)) +ax1.set_xlabel('X') +ax1.set_ylabel('Y') +ax1.set_title('Tree mesh and profile line') + +ax2 = fig.add_axes([0.43, 0.15, 0.5, 0.75]) +ax2.plot( + d, v_true, "k-", + d, v_interp, "bo", + d, np.c_[v_true - v_interp], "ro", +) +ax2.set_ylim([-0.1, 1.3]) +ax2.set_title("Comparison plot") +ax2.set_xlabel("Position along profile") +ax2.legend(( + "true value", "interpolated from centers", "error" +)) + +fig.show() + + +######################################################### +# Constructing and Applying a 2D Averaging Matrix +# ----------------------------------------------- +# +# Here we compute a scalar function at cell centers. +# We then create an averaging operator to approximated the function +# at the faces. We choose to define a scalar function that is +# strongly discontinuous in some places +# to demonstrate how the averaging operator will smooth out +# discontinuities. +# + +# Create mesh and obtain averaging operators +h = 2 * np.ones(50) +mesh = TensorMesh([h, h], x0="CC") + +# Create a variable on cell centers +v = 100.0 * np.ones(mesh.nC) +xy = mesh.gridCC +v[(xy[:, 1] > 0)] = 0.0 +v[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 + +# Create averaging operator +A = mesh.average_cell_to_face # cell centers to faces + +# Apply averaging operator +u = A*v + +# Plot +fig = plt.figure(figsize=(11, 5)) +ax1 = fig.add_subplot(121) +mesh.plot_image(v, ax=ax1) +ax1.set_title("Variable at cell centers") + +ax2 = fig.add_subplot(122) +mesh.plot_image(u, ax=ax2, v_type="F") +ax2.set_title("Averaged to faces") + +fig.show() + diff --git a/tutorials/operators/1_3_interpolation_vector.py b/tutorials/operators/1_3_interpolation_vector.py new file mode 100644 index 000000000..03a158749 --- /dev/null +++ b/tutorials/operators/1_3_interpolation_vector.py @@ -0,0 +1,284 @@ +r""" +Interpolating and Averaging Vector Quantities in 2D and 3D +========================================================== + +Interpolation is used when a discrete quantity is known on the mesh (centers, nodes, edges or faces), +but we would like to estimate its value at locations within the continuous domain. +For any mesh type, *discretize* allows the user to construct a sparse interpolation matrix +for a corresponding set of locations. + +In *discretize*, averaging matrices are constructed when a discrete quantity must be mapped +between centers, nodes, edges or faces. +Averaging matrices are a property of the mesh and are constructed when called. + +In this tutorial, we demonstrate: + + - how to construct interpolation and averaging matrices + - how to apply the interpolation and averaging to vector functions + + +""" + +############################################# +# Background Theory +# ----------------- +# +# We will begin by presenting the theory for 2D interpolation, then extend the +# theory to cover 3D interpolation. +# The components of a vector quantity are discretized to live either on +# their respective mesh faces or edges. Thus different components of the +# vector are being interpolated from different locations. +# +# .. figure:: ../../images/interpolation_2d_vectors.png +# :align: center +# :width: 600 +# +# A tensor mesh in 2D denoting interpolation from faces (left) and edges (right). +# +# Let :math:`\vec{u} (x,y)` be a 2D vector function that is known on the faces of the mesh; +# that is, :math:`u_x` lives on the x-faces and :math:`u_y` lives on the y-faces. +# Note that in the above figure, the x-faces and y-faces both form tensor grids. +# If we want to approximate the components of the vector at a location :math:`(x^*,y^*)`, +# we simply need to treat each component as a scalar function and interpolate it separately. +# +# Where :math:`u_{x,i}` represents the x-component of :math:`\vec{u} (x,y)` on a face :math:`i` being used for the interpolation, +# the approximation of the x-component at :math:`(x^*, y^*)` has the form: +# +# .. math:: +# u_x(x^*, y^*) \approx a_0 u_{x,0} + a_1 u_{x,1} + a_2 u_{x,2} + a_3 u_{x,3} +# :label: operators_interpolation_xvec_coef +# +# For the the y-component, we have a similar representation: +# +# .. math:: +# u_y(x^*, y^*) \approx b_0 u_{y,0} + b_1 u_{y,1} + b_2 u_{y,2} + b_3 u_{y,3} +# +# Where :math:`\boldsymbol{u}` is a vector that organizes the discrete components of :math:`\vec{u} (x,y)` on cell faces, +# and :math:`\boldsymbol{u^*}` is a vector organizing the components of the approximations of :math:`\vec{u}(x,y)` at an arbitrary number of locations, +# the interpolation matrix :math:`\boldsymbol{P}` is defined by: +# +# .. math:: +# \boldsymbol{u^*} \approx \boldsymbol{P \, u} +# :label: operators_interpolation_2d_sys +# +# where +# +# .. math:: +# \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \end{bmatrix} +# \;\;\textrm{,}\;\;\;\; +# \boldsymbol{u^*} = \begin{bmatrix} \boldsymbol{u_x^*} \\ \boldsymbol{u_y^*} \end{bmatrix} +# \;\;\;\;\textrm{and}\;\;\;\; +# \boldsymbol{P} = \begin{bmatrix} \boldsymbol{P_x} & \boldsymbol{0} \\ \boldsymbol{0} & \boldsymbol{P_y} \end{bmatrix} +# +# The interpolation matrix :math:`\boldsymbol{P}` is a sparse block-diagonal matrix. +# The size of the interpolation matrix is the number of locations by the number of faces in the mesh. +# +# **What if we want to interpolate from edges?** +# +# In this case, the derivation is effectively the same. +# However, the locations used for the interpolation are different and +# :math:`\boldsymbol{u}` is now a vector that organizes the discrete components of :math:`\vec{u} (x,y)` on cell edges. +# +# +# **What if we are interpolating a 3D vector?** +# +# In this case, there are 8 face locations or 8 edge locations that are used to approximate +# :math:`\vec{u}(x,y,z)` at each location :math:`(x^*, y^*, z^*)`. +# Similar to expression :eq:`operators_interpolation_xvec_coef` we have: +# +# .. math:: +# \begin{align} +# u_x(x^*, y^*, z^*) & \approx \sum_{i=1}^7 a_i u_{x,i} \\ +# u_y(x^*, y^*, z^*) & \approx \sum_{i=1}^7 b_i u_{y,i} \\ +# u_z(x^*, y^*, z^*) & \approx \sum_{i=1}^7 c_i u_{z,i} +# \end{align} +# +# The interpolation can be expressed similar to that in equation :eq:`operators_interpolation_2d_sys`, +# however: +# +# .. math:: +# \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} +# \;\;\textrm{,}\;\;\;\; +# \boldsymbol{u^*} = \begin{bmatrix} \boldsymbol{u_x^*} \\ \boldsymbol{u_y^*} \\ \boldsymbol{u_z^*} \end{bmatrix} +# \;\;\;\;\textrm{and}\;\;\;\; +# \boldsymbol{P} = \begin{bmatrix} \boldsymbol{P_x} & \boldsymbol{0} & \boldsymbol{0} \\ +# \boldsymbol{0} & \boldsymbol{P_y} & \boldsymbol{0} \\ +# \boldsymbol{0} & \boldsymbol{0} & \boldsymbol{P_z} +# \end{bmatrix} +# + + +############################################### +# Import Packages +# --------------- +# + +from discretize import TensorMesh, TreeMesh +from discretize.utils import refine_tree_xyz +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +mpl.rcParams.update({'font.size':14}) + +# sphinx_gallery_thumbnail_number = 1 + + +############################################################### +# Constructing and Applying a 2D/3D Interpolation Matrix +# ------------------------------------------------------ +# +# Here we discretize a vector quantity to live on the edges of a 2D tensor mesh, +# where the x component lives on x-edges and the y component lives on y-edges. +# We then use interpolation matrices to approximate the vector components +# on the faces of the mesh. That is, we interpolate the x component from x-edges +# to x-faces, and we interpolate the y component from y-edges to y-faces. +# Since the x and y components of vectors are discretized at different locations +# on the mesh, separate interpolation matrices must be constructed for the +# x and y components. +# + +# Create a tensor mesh +h = np.ones(75) +mesh = TensorMesh([h, h], "CC") + +# Define the x and y components of the vector function. +# In this case, the vector function is a circular vortex. +def fun_x(xy): + r = np.sqrt(np.sum(xy ** 2, axis=1)) + return 5. * (-xy[:, 1] / r) * (1 + np.tanh(0.15 * (28.0 - r))) + +def fun_y(xy): + r = np.sqrt(np.sum(xy ** 2, axis=1)) + return 5. * (xy[:, 0] / r) * (1 + np.tanh(0.15 * (28.0 - r))) + +# Evaluate x and y components of the vector on x and y edges, respectively +edges_x = mesh.edges_x +edges_y = mesh.edges_y + +ux_edges = fun_x(edges_x) +uy_edges = fun_y(edges_y) +u_edges = np.r_[ux_edges, uy_edges] + +# Compute true x and y components of the vector on x and y faces, respectively +faces_x = mesh.faces_x +faces_y = mesh.faces_y + +ux_faces = fun_x(faces_x) +uy_faces = fun_y(faces_y) +u_faces = np.r_[ux_faces, uy_faces] + +# Generate the interpolation matricies and interpolate from edges to faces. +# Interpolation matrices from edges and faces assume all vector components +# are defined on their respective edges or faces. Thus an interpolation matrix +# from x-edges will extract the x component values then interpolate to locations. +Px = mesh.get_interpolation_matrix(faces_x, "Ex") +Py = mesh.get_interpolation_matrix(faces_y, "Ey") + +ux_interp = Px*u_edges +uy_interp = Py*u_edges +u_interp = np.r_[ux_interp, uy_interp] + +# Plotting +fig = plt.figure(figsize=(14, 4)) + +ax1 = fig.add_axes([0.05, 0.15, 0.22, 0.75]) +mesh.plot_image( + u_faces, ax=ax1, v_type="F", view="vec", + stream_opts={"color": "w", "density": 1.0}, clim=[0.0, 10.0], +) +ax1.set_title("True Vector on Faces") + +ax2 = fig.add_axes([0.35, 0.15, 0.22, 0.75]) +mesh.plot_image( + u_interp, ax=ax2, v_type="F", view="vec", + stream_opts={"color": "w", "density": 1.0}, clim=[0.0, 10.0], +) +ax2.set_title("Interpolated from Edges to Faces") + +ax3 = fig.add_axes([0.65, 0.15, 0.22, 0.75]) +mesh.plot_image( + u_faces-u_interp, ax=ax3, v_type="F", view="vec", + stream_opts={"color": "w", "density": 1.0}, clim=[0.0, 10.0], +) +ax3.set_title("Error") + +ax4 = fig.add_axes([0.92, 0.15, 0.025, 0.75]) +norm = mpl.colors.Normalize(vmin=0., vmax=10.) +cbar = mpl.colorbar.ColorbarBase( + ax4, norm=norm, orientation="vertical" +) + + +######################################################### +# Constructing and Applying a 2D Averaging Matrix +# ----------------------------------------------- +# +# Here we compute a vector function that lives on the edges. +# We then create an averaging operator to approximate the components +# of the vector at cell centers. +# + +# Create a tensor mesh +h = np.ones(75) +mesh = TensorMesh([h, h], "CC") + +# Define the x and y components of the vector function. +# In this case, the vector function is a circular vortex. +def fun_x(xy): + r = np.sqrt(np.sum(xy ** 2, axis=1)) + return 5. * (-xy[:, 1] / r) * (1 + np.tanh(0.15 * (28.0 - r))) + +def fun_y(xy): + r = np.sqrt(np.sum(xy ** 2, axis=1)) + return 5. * (xy[:, 0] / r) * (1 + np.tanh(0.15 * (28.0 - r))) + +# Evaluate x and y components of the vector on x and y edges, respectively +edges_x = mesh.edges_x +edges_y = mesh.edges_y + +ux_edges = fun_x(edges_x) +uy_edges = fun_y(edges_y) +u_edges = np.r_[ux_edges, uy_edges] + +# Compute true x and y components of the vector at cell centers +centers = mesh.cell_centers + +ux_centers = fun_x(centers) +uy_centers = fun_y(centers) +u_centers = np.r_[ux_centers, uy_centers] + +# Create the averaging operator for a vector from edges to cell centers +A = mesh.average_edge_to_cell_vector + +# Apply the averaging operator +u_average = A*u_edges + +# Plotting +fig = plt.figure(figsize=(14, 4)) + +ax1 = fig.add_axes([0.05, 0.15, 0.22, 0.75]) +mesh.plot_image( + u_edges, ax=ax1, v_type="E", view="vec", + stream_opts={"color": "w", "density": 1.0}, clim=[0.0, 10.0], +) +ax1.set_title("True Vector on Edges") + +ax2 = fig.add_axes([0.35, 0.15, 0.22, 0.75]) +mesh.plot_image( + u_average, ax=ax2, v_type="CCv", view="vec", + stream_opts={"color": "w", "density": 1.0}, clim=[0.0, 10.0], +) +ax2.set_title("Averaged to Cell Centers") + +ax3 = fig.add_axes([0.65, 0.15, 0.22, 0.75]) +mesh.plot_image( + u_centers-u_average, ax=ax3, v_type="CCv", view="vec", + stream_opts={"color": "w", "density": 1.0}, clim=[0.0, 10.0], +) +ax3.set_title("Error") + +ax4 = fig.add_axes([0.92, 0.15, 0.025, 0.75]) +norm = mpl.colors.Normalize(vmin=0., vmax=10.) +cbar = mpl.colorbar.ColorbarBase( + ax4, norm=norm, orientation="vertical" +) diff --git a/tutorials/operators/1_averaging.py b/tutorials/operators/1_averaging.py deleted file mode 100644 index 7a8690163..000000000 --- a/tutorials/operators/1_averaging.py +++ /dev/null @@ -1,197 +0,0 @@ -""" -Averaging Matricies -=================== - -Averaging matricies are used when a discrete variable living on some part of -the mesh (e.g. nodes, centers, edges or faces) must be approximated at other -locations. Averaging matricies are sparse and exist for 1D, 2D and -3D meshes. For each mesh class (*Tensor mesh*, *Tree mesh*, -*Curvilinear mesh*), the set of averaging matricies are properties that are -only constructed when called. - -Here we discuss: - - - How to construct and apply averaging matricies - - Averaging matricies in 1D, 2D and 3D - - Averaging discontinuous functions - - The transpose of an averaging matrix - - -""" - -############################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial. -# - -from discretize import TensorMesh -import matplotlib.pyplot as plt -import numpy as np - -# sphinx_gallery_thumbnail_number = 3 - - -############################################# -# 1D Example -# ---------- -# -# Here we compute a scalar function on cell nodes and average to cell centers. -# We then compute the scalar function at cell centers to validate the -# averaging operator. -# - -# Create a uniform grid -h = 10 * np.ones(20) -mesh = TensorMesh([h], "C") - -# Get node and cell center locations -x_nodes = mesh.vectorNx -x_centers = mesh.vectorCCx - - -# Define a continuous function -def fun(x): - return np.exp(-(x ** 2) / 50 ** 2) - - -# Compute function on nodes and cell centers -v_nodes = fun(x_nodes) -v_centers = fun(x_centers) - -# Create operator and average from nodes to cell centers -A = mesh.aveN2CC -v_approx = A * v_nodes - -# Compare -fig = plt.figure(figsize=(12, 4)) -ax1 = fig.add_axes([0.03, 0.01, 0.3, 0.91]) -ax1.spy(A, markersize=5) -ax1.set_title("Sparse representation of A", pad=10) - -ax2 = fig.add_axes([0.4, 0.06, 0.55, 0.85]) -ax2.plot( - x_centers, - v_centers, - "b-", - x_centers, - v_approx, - "ko", - x_centers, - np.c_[v_centers - v_approx], - "r-", -) -ax2.set_title("Comparison plot") -ax2.legend(("evaluated at centers", "averaged from nodes", "absolute error")) - -fig.show() - -############################################# -# 1D, 2D and 3D Averaging -# ----------------------- -# -# Here we discuss averaging operators in 1D, 2D and 3D. In 1D we can -# average between nodes and cell centers. In higher dimensions, we may need to -# average between nodes, cell centers, faces and edges. For this example we -# describe the averaging operator from faces to cell centers in 1D, 2D and 3D. -# - -# Construct uniform meshes in 1D, 2D and 3D -h = 10 * np.ones(10) -mesh1D = TensorMesh([h], x0="C") -mesh2D = TensorMesh([h, h], x0="CC") -mesh3D = TensorMesh([h, h, h], x0="CCC") - -# Create averaging operators -A1 = mesh1D.aveF2CC # Averages faces (nodes in 1D) to centers -A2 = mesh2D.aveF2CC # Averages from x and y faces to centers -A3 = mesh3D.aveF2CC # Averages from x, y and z faces to centers - -# Plot sparse representation -fig = plt.figure(figsize=(7, 8)) -ax1 = fig.add_axes([0.37, 0.72, 0.2, 0.2]) -ax1.spy(A1, markersize=2.5) -ax1.set_title("Faces to centers in 1D", pad=17) - -ax2 = fig.add_axes([0.17, 0.42, 0.6, 0.22]) -ax2.spy(A2, markersize=1) -ax2.set_title("Faces to centers in 2D", pad=17) - -ax3 = fig.add_axes([0.05, 0, 0.93, 0.4]) -ax3.spy(A3, markersize=0.5) -ax3.set_title("Faces to centers in 3D", pad=17) - -fig.show() - -# Print some properties -print("\n For 1D mesh:") -print("- Number of cells:", str(mesh1D.nC)) -print("- Number of faces:", str(mesh1D.nF)) -print("- Dimensions of operator:", str(mesh1D.nC), "x", str(mesh1D.nF)) -print("- Number of non-zero elements:", str(A1.nnz), "\n") - -print("For 2D mesh:") -print("- Number of cells:", str(mesh2D.nC)) -print("- Number of faces:", str(mesh2D.nF)) -print("- Dimensions of operator:", str(mesh2D.nC), "x", str(mesh2D.nF)) -print("- Number of non-zero elements:", str(A2.nnz), "\n") - -print("For 3D mesh:") -print("- Number of cells:", str(mesh3D.nC)) -print("- Number of faces:", str(mesh3D.nF)) -print("- Dimensions of operator:", str(mesh3D.nC), "x", str(mesh3D.nF)) -print("- Number of non-zero elements:", str(A3.nnz)) - - -###################################################### -# Discontinuous Functions and the Transpose -# ----------------------------------------- -# -# Here we show the effects of applying averaging operators to discontinuous -# functions. We will see that averaging smears the function at -# discontinuities. -# -# The transpose of an averaging operator is also an -# averaging operator. For example, we can average from cell centers to faces -# by taking the transpose of operator that averages from faces to cell centers. -# Note that values on the boundaries are not accurate when applying the -# transpose as an averaging operator. This is also true for staggered grids. -# - -# Create mesh and obtain averaging operators -h = 2 * np.ones(50) -mesh = TensorMesh([h, h], x0="CC") - -A2 = mesh.aveCC2F # cell centers to faces -A3 = mesh.aveN2CC # nodes to cell centers -A4 = mesh.aveF2CC # faces to cell centers - -# Create a variable on cell centers -v = 100.0 * np.ones(mesh.nC) -xy = mesh.gridCC -v[(xy[:, 1] > 0)] = 0.0 -v[(xy[:, 1] < -10.0) & (xy[:, 0] > -10.0) & (xy[:, 0] < 10.0)] = 50.0 - -fig = plt.figure(figsize=(10, 10)) -ax1 = fig.add_subplot(221) -mesh.plotImage(v, ax=ax1) -ax1.set_title("Variable at cell centers") - -# Apply cell centers to faces averaging -ax2 = fig.add_subplot(222) -mesh.plotImage(A2 * v, ax=ax2, v_type="F") -ax2.set_title("Cell centers to faces") - -# Use the transpose to go from cell centers to nodes -ax3 = fig.add_subplot(223) -mesh.plotImage(A3.T * v, ax=ax3, v_type="N") -ax3.set_title("Cell centers to nodes using transpose") - -# Use the transpose to go from cell centers to faces -ax4 = fig.add_subplot(224) -mesh.plotImage(A4.T * v, ax=ax4, v_type="F") -ax4.set_title("Cell centers to faces using transpose") - -fig.show() diff --git a/tutorials/operators/2_differential.py b/tutorials/operators/2_differential.py deleted file mode 100644 index cdf6ce58f..000000000 --- a/tutorials/operators/2_differential.py +++ /dev/null @@ -1,326 +0,0 @@ -""" -Differential Operators -====================== - -For discretized quantities living on a mesh, sparse matricies can be used to -approximate the following differential operators: - - - gradient: :math:`\\nabla \phi` - - divergence: :math:`\\nabla \cdot \mathbf{v}` - - curl: :math:`\\nabla \\times \mathbf{v}` - - scalar Laplacian: :math:`\Delta \mathbf{v}` - -Numerical differential operators exist for 1D, 2D and 3D meshes. For each mesh -class (*Tensor mesh*, *Tree mesh*, *Curvilinear mesh*), the set of numerical -differential operators are properties that are only constructed when called. - -Here we demonstrate: - - - How to construct and apply numerical differential operators - - Mapping and dimensions - - Applications for the transpose - - -""" - -############################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial. -# - -from discretize import TensorMesh, TreeMesh -import matplotlib.pyplot as plt -import numpy as np - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# 1D Example -# ---------- -# -# Here we compute a scalar function on cell nodes and differentiate with -# respect to x. We then compute the analytic derivative of function to validate -# the numerical differentiation. -# - -# Create a uniform grid -h = np.ones(20) -mesh = TensorMesh([h], "C") - -# Get node and cell center locations -x_nodes = mesh.vectorNx -x_centers = mesh.vectorCCx - -# Compute function on nodes and derivative at cell centers -v = np.exp(-(x_nodes ** 2) / 4 ** 2) -dvdx = -(2 * x_centers / 4 ** 2) * np.exp(-(x_centers ** 2) / 4 ** 2) - -# Derivative in x (gradient in 1D) from nodes to cell centers -G = mesh.nodalGrad -dvdx_approx = G * v - -# Compare -fig = plt.figure(figsize=(12, 4)) -ax1 = fig.add_axes([0.03, 0.01, 0.3, 0.89]) -ax1.spy(G, markersize=5) -ax1.set_title("Sparse representation of G", pad=10) - -ax2 = fig.add_axes([0.4, 0.06, 0.55, 0.85]) -ax2.plot(x_nodes, v, "b-", x_centers, dvdx, "r-", x_centers, dvdx_approx, "ko") -ax2.set_title("Comparison plot") -ax2.legend(("function", "analytic derivative", "numeric derivative")) - -fig.show() - - -############################################# -# Mapping and Dimensions -# ---------------------- -# -# When discretizing and solving differential equations, it is -# natural for certain quantities to be defined at particular locations on the -# mesh; e.g.: -# -# - Scalar quantities on nodes or at cell centers -# - Vector quantities on cell edges or on cell faces -# -# As such, numerical differential operators frequently map from one part of -# the mesh to another. For example, the gradient acts on a scalar quantity -# an results in a vector quantity. As a result, the numerical gradient -# operator may map from nodes to edges or from cell centers to faces. -# -# Here we explore the dimensions of the gradient, divergence and curl -# operators for a 3D tensor mesh. This can be extended to other mesh types. -# - -# Create a uniform grid -h = np.ones(20) -mesh = TensorMesh([h, h, h], "CCC") - -# Get differential operators -GRAD = mesh.nodalGrad # Gradient from nodes to edges -DIV = mesh.faceDiv # Divergence from faces to cell centers -CURL = mesh.edgeCurl # Curl edges to cell centers - - -fig = plt.figure(figsize=(9, 8)) - -ax1 = fig.add_axes([0.07, 0, 0.20, 0.7]) -ax1.spy(GRAD, markersize=0.5) -ax1.set_title("Gradient (nodes to edges)") - -ax2 = fig.add_axes([0.345, 0.73, 0.59, 0.185]) -ax2.spy(DIV, markersize=0.5) -ax2.set_title("Divergence (faces to centers)", pad=20) - -ax3 = fig.add_axes([0.31, 0.05, 0.67, 0.60]) -ax3.spy(CURL, markersize=0.5) -ax3.set_title("Curl (edges to faces)") - -fig.show() - -# Print some properties -print("\n Gradient:") -print("- Number of nodes:", str(mesh.nN)) -print("- Number of edges:", str(mesh.nE)) -print("- Dimensions of operator:", str(mesh.nE), "x", str(mesh.nN)) -print("- Number of non-zero elements:", str(GRAD.nnz), "\n") - -print("Divergence:") -print("- Number of faces:", str(mesh.nF)) -print("- Number of cells:", str(mesh.nC)) -print("- Dimensions of operator:", str(mesh.nC), "x", str(mesh.nF)) -print("- Number of non-zero elements:", str(DIV.nnz), "\n") - -print("Curl:") -print("- Number of faces:", str(mesh.nF)) -print("- Number of edges:", str(mesh.nE)) -print("- Dimensions of operator:", str(mesh.nE), "x", str(mesh.nF)) -print("- Number of non-zero elements:", str(CURL.nnz)) - - -############################################# -# 2D Example -# ---------- -# -# Here we apply the gradient, divergence and curl operators to a set of -# functions defined on a 2D tensor mesh. We then plot the results. -# - -# Create a uniform grid -h = np.ones(20) -mesh = TensorMesh([h, h], "CC") - -# Get differential operators -GRAD = mesh.nodalGrad # Gradient from nodes to edges -DIV = mesh.faceDiv # Divergence from faces to cell centers -CURL = mesh.edgeCurl # Curl edges to cell centers (goes to faces in 3D) - -# Evaluate gradient of a scalar function -nodes = mesh.gridN -u = np.exp(-(nodes[:, 0] ** 2 + nodes[:, 1] ** 2) / 4 ** 2) -grad_u = GRAD * u - -# Evaluate divergence of a vector function in x and y -faces_x = mesh.gridFx -faces_y = mesh.gridFy - -vx = (faces_x[:, 0] / np.sqrt(np.sum(faces_x ** 2, axis=1))) * np.exp( - -(faces_x[:, 0] ** 2 + faces_x[:, 1] ** 2) / 6 ** 2 -) - -vy = (faces_y[:, 1] / np.sqrt(np.sum(faces_y ** 2, axis=1))) * np.exp( - -(faces_y[:, 0] ** 2 + faces_y[:, 1] ** 2) / 6 ** 2 -) - -v = np.r_[vx, vy] -div_v = DIV * v - -# Evaluate curl of a vector function in x and y -edges_x = mesh.gridEx -edges_y = mesh.gridEy - -wx = (-edges_x[:, 1] / np.sqrt(np.sum(edges_x ** 2, axis=1))) * np.exp( - -(edges_x[:, 0] ** 2 + edges_x[:, 1] ** 2) / 6 ** 2 -) - -wy = (edges_y[:, 0] / np.sqrt(np.sum(edges_y ** 2, axis=1))) * np.exp( - -(edges_y[:, 0] ** 2 + edges_y[:, 1] ** 2) / 6 ** 2 -) - -w = np.r_[wx, wy] -curl_w = CURL * w - -# Plot Gradient of u -fig = plt.figure(figsize=(10, 5)) - -ax1 = fig.add_subplot(121) -mesh.plotImage(u, ax=ax1, v_type="N") -ax1.set_title("u at cell centers") - -ax2 = fig.add_subplot(122) -mesh.plotImage( - grad_u, ax=ax2, v_type="E", view="vec", stream_opts={"color": "w", "density": 1.0} -) -ax2.set_title("gradient of u on edges") - -fig.show() - -# Plot divergence of v -fig = plt.figure(figsize=(10, 5)) - -ax1 = fig.add_subplot(121) -mesh.plotImage( - v, ax=ax1, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} -) -ax1.set_title("v at cell faces") - -ax2 = fig.add_subplot(122) -mesh.plotImage(div_v, ax=ax2) -ax2.set_title("divergence of v at cell centers") - -fig.show() - -# Plot curl of w -fig = plt.figure(figsize=(10, 5)) - -ax1 = fig.add_subplot(121) -mesh.plotImage( - w, ax=ax1, v_type="E", view="vec", stream_opts={"color": "w", "density": 1.0} -) -ax1.set_title("w at cell edges") - -ax2 = fig.add_subplot(122) -mesh.plotImage(curl_w, ax=ax2) -ax2.set_title("curl of w at cell centers") - -fig.show() - -######################################################### -# Tree Mesh Divergence -# -------------------- -# -# For a tree mesh, there needs to be special attention taken for the hanging -# faces to achieve second order convergence for the divergence operator. -# Although the divergence cannot be constructed through Kronecker product -# operations, the initial steps are exactly the same for calculating the -# stencil, volumes, and areas. This yields a divergence defined for every -# cell in the mesh using all faces. There is, however, redundant information -# when hanging faces are included. -# - -mesh = TreeMesh([[(1, 16)], [(1, 16)]], levels=4) -mesh.insert_cells(np.array([5.0, 5.0]), np.array([3])) -mesh.number() - -fig = plt.figure(figsize=(10, 10)) - -ax1 = fig.add_subplot(211) - -mesh.plotGrid(centers=True, nodes=False, ax=ax1) -ax1.axis("off") -ax1.set_title("Simple QuadTree Mesh") -ax1.set_xlim([-1, 17]) -ax1.set_ylim([-1, 17]) - -for ii, loc in zip(range(mesh.nC), mesh.gridCC): - ax1.text(loc[0] + 0.2, loc[1], "{0:d}".format(ii), color="r") - -ax1.plot(mesh.gridFx[:, 0], mesh.gridFx[:, 1], "g>") -for ii, loc in zip(range(mesh.nFx), mesh.gridFx): - ax1.text(loc[0] + 0.2, loc[1], "{0:d}".format(ii), color="g") - -ax1.plot(mesh.gridFy[:, 0], mesh.gridFy[:, 1], "m^") -for ii, loc in zip(range(mesh.nFy), mesh.gridFy): - ax1.text(loc[0] + 0.2, loc[1] + 0.2, "{0:d}".format((ii + mesh.nFx)), color="m") - -ax2 = fig.add_subplot(212) -ax2.spy(mesh.faceDiv) -ax2.set_title("Face Divergence") -ax2.set_ylabel("Cell Number") -ax2.set_xlabel("Face Number") - - -######################################################### -# Vector Calculus Identities -# -------------------------- -# -# Here we show that vector calculus identities hold for the discrete -# differential operators. Namely that for a scalar quantity :math:`\phi` and -# a vector quantity :math:`\mathbf{v}`: -# -# .. math:: -# \begin{align} -# &\nabla \times (\nabla \phi ) = 0 \\ -# &\nabla \cdot (\nabla \times \mathbf{v}) = 0 -# \end{align} -# -# -# We do this by computing the CURL*GRAD and DIV*CURL matricies. We then -# plot the sparse representations and show neither contain any non-zero -# entries; **e.g. each is just a matrix of zeros**. -# - -# Create a mesh -h = 5 * np.ones(20) -mesh = TensorMesh([h, h, h], "CCC") - -# Get operators -GRAD = mesh.nodalGrad # nodes to edges -DIV = mesh.faceDiv # faces to centers -CURL = mesh.edgeCurl # edges to faces - -# Plot -fig = plt.figure(figsize=(11, 7)) - -ax1 = fig.add_axes([0.12, 0.1, 0.2, 0.8]) -ax1.spy(CURL * GRAD, markersize=0.5) -ax1.set_title("CURL*GRAD") - -ax2 = fig.add_axes([0.35, 0.64, 0.6, 0.25]) -ax2.spy(DIV * CURL, markersize=0.5) -ax2.set_title("DIV*CURL", pad=20) diff --git a/tutorials/operators/2_gradient.py b/tutorials/operators/2_gradient.py new file mode 100644 index 000000000..f456f980d --- /dev/null +++ b/tutorials/operators/2_gradient.py @@ -0,0 +1,253 @@ +r""" +Gradient Operator +================= + +For geophysical problems, the relationship between two physical quantities may include the gradient: + +.. math:: + \nabla \phi = \dfrac{\partial \phi}{\partial x}\hat{x} + \dfrac{\partial \phi}{\partial y}\hat{y} + \dfrac{\partial \phi}{\partial z}\hat{z} + +For discretized quantities living on 1D, 2D or 3D meshes, sparse matricies can be used to +approximate the gradient operator. For each mesh type, the gradient +operator is a property that is only constructed when called. + +This tutorial focusses on: + + - how to construct the gradient operator + - applying the gradient operator to a discrete quantity + - mapping and dimensions or the gradient operator + +""" + +##################################################################### +# Background Theory +# ----------------- +# +# Let us define a continuous scalar function :math:`\phi` and a continuous vector function :math:`\vec{u}` such that: +# +# .. math:: +# \vec{u} = \nabla \phi +# +# And let :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` be the discrete representations of :math:`\phi` and :math:`\vec{u}` +# that live on the mesh. Provided we know the discrete values :math:`\boldsymbol{\phi}`, +# our goal is to use discrete differentiation to approximate the vector components of :math:`\boldsymbol{u}`. +# We begin by considering a single cell (2D or 3D). We let the indices :math:`i`, :math:`j` and :math:`k` +# denote positions along the x, y and z axes, respectively. +# +# .. figure:: ../../images/gradient_discretization.png +# :align: center +# :width: 600 +# +# Discretization for approximating the gradient on the edges of a single 2D cell (left) and 3D cell (right). +# +# As we will see, it makes the most sense for :math:`\boldsymbol{\phi}` to live at the cell nodes and +# for the components of :math:`\boldsymbol{u}` to live on corresponding edges. If :math:`\phi` lives on the nodes, then: +# +# - the partial derivative :math:`\dfrac{\partial \phi}{\partial x}\hat{x}` lives on x-edges, +# - the partial derivative :math:`\dfrac{\partial \phi}{\partial y}\hat{y}` lives on y-edges, and +# - the partial derivative :math:`\dfrac{\partial \phi}{\partial z}\hat{z}` lives on z-edges +# +# Thus to approximate the gradient of :math:`\phi`, +# we simply need to take discrete derivatives of :math:`\phi` with respect to :math:`x`, :math:`y` and :math:`z`, +# and organize the resulting vector components on the corresponding edges. +# Let :math:`h_x`, :math:`h_y` and :math:`h_z` represent the dimension of the cell along the x, y and +# z directions, respectively. +# +# **In 2D**, the value of :math:`\phi` at 4 node locations is used to approximate the vector components of the +# gradient at 4 edges locations (2 x-edges and 2 y-edges) as follows: +# +# .. math:: +# \begin{align} +# u_x \Big ( i+\frac{1}{2},j \Big ) \approx \; & \frac{\phi (i+1,j) - \phi (i,j)}{h_x} \\ +# u_x \Big ( i+\frac{1}{2},j+1 \Big ) \approx \; & \frac{\phi (i+1,j+1) - \phi (i,j+1)}{h_x} \\ +# u_y \Big ( i,j+\frac{1}{2} \Big ) \approx \; & \frac{\phi (i,j+1) - \phi (i,j)}{h_y} \\ +# u_y \Big ( i+1,j+\frac{1}{2} \Big ) \approx \; & \frac{\phi (i+1,j+1) - \phi (i+1,j)}{h_y} +# \end{align} +# +# **In 3D**, the value of :math:`\phi` at 8 node locations is used to approximate the vector components of the +# gradient at 12 edges locations (4 x-edges, 4 y-edges and 4 z-edges). An example of the approximation +# for each vector component is given below: +# +# .. math:: +# \begin{align} +# u_x \Big ( i+\frac{1}{2},j,k \Big ) \approx \; & \frac{\phi (i+1,j,k) - \phi (i,j,k)}{h_x} \\ +# u_y \Big ( i,j+\frac{1}{2},k \Big ) \approx \; & \frac{\phi (i,j+1,k) - \phi (i,j,k)}{h_y} \\ +# u_z \Big ( i,j,k+\frac{1}{2} \Big ) \approx \; & \frac{\phi (i,j,k+1) - \phi (i,j,k)}{h_z} +# \end{align} +# +# +# Ultimately we are trying to approximate the vector components of the gradient at all edges of a mesh. +# Adjacent cells share nodes. If :math:`\phi` is continuous at the nodes, then :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` +# can be related by a sparse matrix-vector product: +# +# .. math:: +# \boldsymbol{u} = \boldsymbol{G \, \phi} +# +# where :math:`\boldsymbol{G}` is the gradient matrix that maps from nodes to edges, +# :math:`\boldsymbol{\phi}` is a vector containing :math:`\phi` at all nodes, +# and :math:`\boldsymbol{u}` stores the components of :math:`\vec{u}` on cell edges as a vector of the form: +# +# .. math:: +# \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} +# +# + +############################################### +# Import Packages +# --------------- +# + +from discretize import TensorMesh, TreeMesh +from discretize.utils import mkvc, refine_tree_xyz +import matplotlib.pyplot as plt +import numpy as np + +# sphinx_gallery_thumbnail_number = 2 + + +############################################# +# 1D Example +# ---------- +# +# For the 1D case, the gradient operator simply approximates the derivative +# of a function. Here we compute a scalar function on cell nodes and +# differentiate with respect to x. We then compute the analytic +# derivative of function to validate the numerical differentiation. +# + +# Create a uniform grid +h = np.ones(20) +mesh = TensorMesh([h], "C") + +# Get node and cell center locations +x_nodes = mesh.vectorNx +x_centers = mesh.vectorCCx + +# Compute function on nodes and derivative at cell centers +v = np.exp(-(x_nodes ** 2) / 4 ** 2) +dvdx = -(2 * x_centers / 4 ** 2) * np.exp(-(x_centers ** 2) / 4 ** 2) + +# Derivative in x (gradient in 1D) from nodes to cell centers +G = mesh.nodalGrad +dvdx_approx = G * v + +# Compare +fig = plt.figure(figsize=(12, 4)) +ax1 = fig.add_axes([0.1, 0.02, 0.25, 0.8]) +ax1.spy(G, markersize=5) +ax1.set_title("Spy plot of gradient", pad=10) + +ax2 = fig.add_axes([0.4, 0.06, 0.55, 0.85]) +ax2.plot(x_nodes, v, "b-", x_centers, dvdx, "r-", x_centers, dvdx_approx, "ko") +ax2.set_title("Comparison plot") +ax2.legend(("function", "analytic derivative", "numeric derivative")) + +fig.show() + +############################################# +# 2D Example +# ---------- +# +# Here we apply the gradient to a scalar function that lives +# on the nodes of a 2D tensor mesh. This produces a +# a discrete approximation of the gradient that lives +# on the edges of the mesh. +# + +# Create a uniform grid +h = np.ones(20) +mesh = TensorMesh([h, h], "CC") + +# Get the gradient +GRAD = mesh.nodal_gradient # Gradient from nodes to edges + +# Evaluate gradient of a scalar function +nodes = mesh.nodes +u = np.exp(-(nodes[:, 0] ** 2 + nodes[:, 1] ** 2) / 4 ** 2) +grad_u = GRAD * u + +# Plot Gradient of u +fig = plt.figure(figsize=(11, 5)) + +ax1 = fig.add_subplot(121) +mesh.plotImage(u, ax=ax1, v_type="N") +ax1.set_title("u on cell nodes") + +ax2 = fig.add_subplot(122) +mesh.plotImage( + grad_u, ax=ax2, v_type="E", view="vec", stream_opts={"color": "w", "density": 1.0} +) +ax2.set_title("gradient of u on edges") + +fig.show() + + +############################################# +# Mapping and Dimensions +# ---------------------- +# +# When discretizing and solving differential equations, it is +# natural for: +# +# - Scalar quantities on nodes or at cell centers +# - Vector quantities on cell edges or on cell faces +# +# As a result, the gradient operator will map from one part of +# the mesh to another; either nodes to edges or cell centers to faces. +# +# Here we construct the gradient operator for a tensor mesh and for +# a tree mesh. By plotting the operators on a spy plot, we gain +# an understanding of the dimensions +# of the gradient operator and its structure. +# + +# Create a basic tensor mesh +h = np.ones(20) +tensor_mesh = TensorMesh([h, h], "CC") + +# Create a basic tree mesh +h = np.ones(32) +tree_mesh = TreeMesh([h, h], origin="CC") +xp, yp = np.meshgrid([-8., 8.], [-8., 8.]) +xy = np.c_[mkvc(xp), mkvc(yp)] +tree_mesh = refine_tree_xyz(tree_mesh, xy, octree_levels=[1, 1], method="box", finalize=False) +tree_mesh.finalize() + +# Plot meshes +fig, ax = plt.subplots(1, 2, figsize=(11, 5)) + +tensor_mesh.plot_grid(ax=ax[0]) +ax[0].set_title("Tensor Mesh") + +tree_mesh.plot_grid(ax=ax[1]) +ax[1].set_title("Tree Mesh") + +# Construct gradient operators +tensor_gradient = tensor_mesh.cell_gradient # 2D gradient from centers to faces +tree_gradient = tree_mesh.nodal_gradient # 2D gradient from nodes to edges + +# Plot gradient operators +fig = plt.figure(figsize=(7, 5)) + +ax1 = fig.add_axes([0.15, 0.05, 0.30, 0.85]) +ax1.spy(tensor_gradient, markersize=0.5) +ax1.set_title("Tensor Gradient") + +ax2 = fig.add_axes([0.65, 0.05, 0.30, 0.85]) +ax2.spy(tree_gradient, markersize=0.5) +ax2.set_title("Tree Gradient") + +fig.show() + +# Print some properties +print("\n Centers to Faces on Tensor Mesh:") +print("- Number of nodes:", str(tensor_mesh.nN)) +print("- Number of edges:", str(tensor_mesh.nE)) +print("- Dimensions of operator:", str(tensor_mesh.nE), "x", str(tensor_mesh.nN)) +print("- Number of non-zero elements:", str(tensor_gradient.nnz), "\n") + +print("\n Nodes to Edges on Tree Mesh:") +print("- Number of nodes:", str(tree_mesh.nN)) +print("- Number of edges:", str(tree_mesh.nE)) +print("- Dimensions of operator:", str(tree_mesh.nE), "x", str(tree_mesh.nN)) +print("- Number of non-zero elements:", str(tree_gradient.nnz), "\n") diff --git a/tutorials/operators/3_divergence.py b/tutorials/operators/3_divergence.py new file mode 100644 index 000000000..83a6eebfa --- /dev/null +++ b/tutorials/operators/3_divergence.py @@ -0,0 +1,215 @@ +r""" +Divergence Operator +=================== + +For geophysical problems, the relationship between two physical quantities may include the divergence: + +.. math:: + \nabla \cdot \vec{u} = \dfrac{\partial u_x}{\partial x} + \dfrac{\partial u_y}{\partial y} + \dfrac{\partial u_y}{\partial y} + +For discretized quantities living on 2D or 3D meshes, sparse matricies can be used to +approximate the divergence operator. For each mesh type, the divergence +operator is a property that is only constructed when called. + +This tutorial focusses on: + + - how to construct the divergence operator + - applying the divergence operator to a discrete quantity + - mapping and dimensions + +""" + + +##################################################################### +# Background Theory +# ----------------- +# +# Let us define a continuous scalar function :math:`\phi` and a continuous vector function :math:`\vec{u}` such that: +# +# .. math:: +# \phi = \nabla \cdot \vec{u} +# +# And let :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` be the discrete representations of :math:`\phi` and :math:`\vec{u}` +# that live on the mesh. Provided we know the discrete values :math:`\boldsymbol{u}`, +# our goal is to use discrete differentiation to approximate the values of :math:`\boldsymbol{\phi}`. +# We begin by considering a single cell (2D or 3D). We let the indices :math:`i`, :math:`j` and :math:`k` +# denote positions along the x, y and z axes, respectively. +# +# .. figure:: ../../images/divergence_discretization.png +# :align: center +# :width: 600 +# +# Discretization for approximating the divergence at the center of a single 2D cell (left) and 3D cell (right). +# +# As we will see, it makes the most sense for :math:`\boldsymbol{\phi}` to live at the cell centers and +# for the components of :math:`\boldsymbol{u}` to live on the faces. If :math:`u_x` lives on x-faces, then its discrete +# derivative with respect to :math:`x` lives at the cell center. And if :math:`u_y` lives on y-faces its discrete +# derivative with respect to :math:`y` lives at the cell center. Likewise for :math:`u_z`. Thus to approximate the +# divergence of :math:`\vec{u}` at the cell center, we simply need to sum the discrete derivatives of :math:`u_x`, :math:`u_y` +# and :math:`u_z` that are defined at the cell center. Where :math:`h_x`, :math:`h_y` and :math:`h_z` represent the dimension of the cell along the x, y and +# z directions, respectively: +# +# .. math:: +# \begin{align} +# \mathbf{In \; 2D:} \;\; \phi(i,j) \approx \; & \frac{u_x(i,j+\frac{1}{2}) - u_x(i,j-\frac{1}{2})}{h_x} \\ +# & + \frac{u_y(i+\frac{1}{2},j) - u_y(i-\frac{1}{2},j)}{h_y} +# \end{align} +# +# | +# +# .. math:: +# \begin{align} +# \mathbf{In \; 3D:} \;\; \phi(i,j,k) \approx \; & \frac{u_x(i+\frac{1}{2},j,k) - u_x(i-\frac{1}{2},j,k)}{h_x} \\ +# & + \frac{u_y(i,j+\frac{1}{2},k) - u_y(i,j-\frac{1}{2},k)}{h_y} \\ +# & + \frac{u_z(i,j,k+\frac{1}{2}) - u_z(i,j,k-\frac{1}{2})}{h_z} +# \end{align} +# +# +# Ultimately we are trying to approximate the divergence at the center of every cell in a mesh. +# Adjacent cells share faces. If the components :math:`u_x`, :math:`u_y` and :math:`u_z` are +# continuous across their respective faces, then :math:`\boldsymbol{\phi}` and :math:`\boldsymbol{u}` +# can be related by a sparse matrix-vector product: +# +# .. math:: +# \boldsymbol{\phi} = \boldsymbol{D \, u} +# +# where :math:`\boldsymbol{D}` is the divergence matrix from faces to cell centers, +# :math:`\boldsymbol{\phi}` is a vector containing the discrete approximations of :math:`\phi` at all cell centers, +# and :math:`\boldsymbol{u}` stores the components of :math:`\vec{u}` on cell faces as a vector of the form: +# +# .. math:: +# \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} +# + +############################################### +# +# Import Packages +# --------------- +# +# Here we import the packages required for this tutorial. +# + +from discretize import TensorMesh, TreeMesh +from discretize.utils import mkvc, refine_tree_xyz +import matplotlib.pyplot as plt +import numpy as np + +# sphinx_gallery_thumbnail_number = 1 + +############################################# +# 2D Example +# ---------- +# +# Here we apply the divergence operator to a vector +# that lives on the faces of a 2D tensor mesh. +# We then plot the results. +# + +# Create a uniform grid +h = np.ones(20) +mesh = TensorMesh([h, h], "CC") + +# Get divergence +DIV = mesh.face_divergence # Divergence from faces to cell centers + +# Evaluate divergence of a vector function in x and y +faces_x = mesh.gridFx +faces_y = mesh.gridFy + +vx = (faces_x[:, 0] / np.sqrt(np.sum(faces_x ** 2, axis=1))) * np.exp( + -(faces_x[:, 0] ** 2 + faces_x[:, 1] ** 2) / 6 ** 2 +) + +vy = (faces_y[:, 1] / np.sqrt(np.sum(faces_y ** 2, axis=1))) * np.exp( + -(faces_y[:, 0] ** 2 + faces_y[:, 1] ** 2) / 6 ** 2 +) + +v = np.r_[vx, vy] +div_v = DIV * v + +# Plot divergence of v +fig = plt.figure(figsize=(11, 5)) + +ax1 = fig.add_subplot(121) +mesh.plotImage( + v, ax=ax1, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} +) +ax1.set_title("v at cell faces") + +ax2 = fig.add_subplot(122) +mesh.plotImage(div_v, ax=ax2) +ax2.set_title("divergence of v at cell centers") + +fig.show() + + +############################################# +# Mapping and Dimensions +# ---------------------- +# +# When discretizing and solving differential equations, it is +# natural for: +# +# - Scalar quantities on nodes or at cell centers +# - Vector quantities on cell edges or on cell faces +# +# As a result, the divergence operator will map from one part of +# the mesh to another; either edges to nodes or faces to cell centers. +# +# Here we construct the divergence operator for a tensor mesh and for +# a tree mesh. By plotting the operators on a spy plot, we gain +# an understanding of the dimensions +# of the divergence operator and its structure. +# + +# Create a basic tensor mesh +h = np.ones(20) +tensor_mesh = TensorMesh([h, h], "CC") + +# Create a basic tree mesh +h = np.ones(32) +tree_mesh = TreeMesh([h, h], origin="CC") +xp, yp = np.meshgrid([-8., 8.], [-8., 8.]) +xy = np.c_[mkvc(xp), mkvc(yp)] +tree_mesh = refine_tree_xyz(tree_mesh, xy, octree_levels=[1, 1], method="box", finalize=False) +tree_mesh.finalize() + +# Plot meshes +fig, ax = plt.subplots(1, 2, figsize=(11, 5)) + +tensor_mesh.plot_grid(ax=ax[0]) +ax[0].set_title("Tensor Mesh") + +tree_mesh.plot_grid(ax=ax[1]) +ax[1].set_title("Tree Mesh") + +# Construct divergence operators +tensor_divergence = tensor_mesh.face_divergence # 2D divergence from faces to centers +tree_divergence = tree_mesh.face_divergence # 2D divergence from faces to centers + +# Plot divergence operators +fig = plt.figure(figsize=(6, 5)) + +ax1 = fig.add_axes([0.15, 0.55, 0.8, 0.35]) +ax1.spy(tensor_divergence, markersize=0.5) +ax1.set_title("2D Tensor Mesh Divergence") + +ax2 = fig.add_axes([0.15, 0.05, 0.8, 0.35]) +ax2.spy(tree_divergence, markersize=0.5) +ax2.set_title("2D Tree Mesh Divergence") + +fig.show() + +print("Faces to Centers on Tensor Mesh:") +print("- Number of faces:", str(tensor_mesh.nF)) +print("- Number of cells:", str(tensor_mesh.nC)) +print("- Dimensions of operator:", str(tensor_mesh.nC), "x", str(tensor_mesh.nF)) +print("- Number of non-zero elements:", str(tensor_divergence.nnz), "\n") + +print("Faces to Centers on Tree Mesh:") +print("- Number of faces:", str(tree_mesh.nF)) +print("- Number of cells:", str(tree_mesh.nC)) +print("- Dimensions of operator:", str(tree_mesh.nC), "x", str(tree_mesh.nF)) +print("- Number of non-zero elements:", str(tree_divergence.nnz), "\n") + + diff --git a/tutorials/operators/4_curl.py b/tutorials/operators/4_curl.py new file mode 100644 index 000000000..b2e237b2a --- /dev/null +++ b/tutorials/operators/4_curl.py @@ -0,0 +1,213 @@ +r""" +Curl Opertor +============ + +For geophysical problems, the relationship between two physical quantities may include the curl: + +.. math:: + \nabla \times \vec{u} = \Bigg ( \dfrac{\partial u_y}{\partial z} - \dfrac{\partial u_z}{\partial y} \Bigg )\hat{x} - \Bigg ( \dfrac{\partial u_x}{\partial z} - \dfrac{\partial u_z}{\partial x} \Bigg )\hat{y} + \Bigg ( \dfrac{\partial u_x}{\partial y} - \dfrac{\partial u_y}{\partial x} \Bigg )\hat{z} + +For discretized quantities living on 2D or 3D meshes, sparse matricies can be used to +approximate the curl operator. For each mesh type, the curl +operator is a property that is only constructed when called. + +This tutorial focusses on: + + - how to construct the curl operator + - applying the curl operator to a discrete quantity + - mapping and dimensions + +""" + +##################################################################### +# Background Theory +# ----------------- +# +# Let us define two continuous vector functions :math:`\vec{u}` and :math:`\vec{w}` such that: +# +# .. math:: +# \vec{w} = \nabla \times \vec{u} +# +# And let :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` be the discrete representations of :math:`\vec{u}` and :math:`\vec{w}` +# that live on the mesh. Provided we know the discrete values :math:`\boldsymbol{u}`, +# our goal is to use discrete differentiation to approximate the vector components of :math:`\boldsymbol{w}`. +# We begin by considering a single 3D cell. We let the indices :math:`i`, :math:`j` and :math:`k` +# denote positions along the x, y and z axes, respectively. +# +# .. figure:: ../../images/curl_discretization.png +# :align: center +# :width: 800 +# +# Discretization for approximating the x, y and z components of the curl on the respective faces of a 3D cell. +# +# +# As we will see, it makes the most sense for the vector components of :math:`\boldsymbol{u}` to live on the edges +# for the vector components of :math:`\boldsymbol{w}` to live the faces. In this case, we need to approximate: +# +# +# - the partial derivatives :math:`\dfrac{\partial u_y}{\partial z}` and :math:`\dfrac{\partial u_z}{\partial y}` to compute :math:`w_x`, +# - the partial derivatives :math:`\dfrac{\partial u_x}{\partial z}` and :math:`\dfrac{\partial u_z}{\partial x}` to compute :math:`w_y`, and +# - the partial derivatives :math:`\dfrac{\partial u_x}{\partial y}` and :math:`\dfrac{\partial u_y}{\partial x}` to compute :math:`w_z` +# +# **In 3D**, discrete values at 12 edge locations (4 x-edges, 4 y-edges and 4 z-edges) are used to +# approximate the vector components of the curl at 6 face locations (2 x-faces, 2-faces and 2 z-faces). +# An example of the approximation for each vector component is given below: +# +# .. math:: +# \begin{align} +# w_x \Big ( i,j \! +\!\!\frac{1}{2},k \! +\!\!\frac{1}{2} \Big ) \!\approx\! \; & +# \!\Bigg ( \! \frac{u_z (i,j \! +\!\!1,k \! +\!\!\frac{1}{2}) \! -\! u_z (i,j,k \! +\!\!\frac{1}{2})}{h_y} \Bigg) \! +# \! -\! \!\Bigg ( \! \frac{u_y (i,j \! +\!\!\frac{1}{2},k \! +\!\!1) \! -\! u_y (i,j \! +\!\!\frac{1}{2},k)}{h_z} \Bigg) \! \\ +# & \\ +# w_y \Big ( i \! +\!\!\frac{1}{2},j,k \! +\!\!\frac{1}{2} \Big ) \!\approx\! \; & +# \!\Bigg ( \! \frac{u_x (i \! +\!\!\frac{1}{2},j,k \! +\!\!1) \! -\! u_x (i \! +\!\!\frac{1}{2},j,k)}{h_z} \Bigg) +# \! -\! \!\Bigg ( \! \frac{u_z (i \! +\!\!1,j,k \! +\!\!\frac{1}{2}) \! -\! u_z (i,j,k \! +\!\!\frac{1}{2})}{h_x} \Bigg) \! \\ +# & \\ +# w_z \Big ( i \! +\!\!\frac{1}{2},j \! +\!\!\frac{1}{2},k \Big ) \!\approx\! \; & +# \!\Bigg ( \! \frac{u_y (i \! +\!\!1,j \! +\!\!\frac{1}{2},k) \! -\! u_y (i,j \! +\!\!\frac{1}{2},k)}{h_x} \Bigg ) +# \! -\! \!\Bigg ( \! \frac{u_x (i \! +\!\!\frac{1}{2},j \! +\!\!1,k) \! -\! u_x (i \! +\!\!\frac{1}{2},j,k)}{h_y} \Bigg) \! +# \end{align} +# +# +# Ultimately we are trying to approximate the curl on all the faces within a mesh. +# Adjacent cells share edges. If the components :math:`u_x`, :math:`u_y` and :math:`u_z` are +# continuous across at the edges, then :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` +# can be related by a sparse matrix-vector product: +# +# .. math:: +# \boldsymbol{w} = \boldsymbol{C \, u} +# +# where :math:`\boldsymbol{C}` is the curl matrix from edges to faces, +# :math:`\boldsymbol{u}` is a vector that stores the components of :math:`\vec{u}` on cell edges, +# and :math:`\boldsymbol{w}` is a vector that stores the components of :math:`\vec{w}` on cell faces such that: +# +# .. math:: +# \boldsymbol{u} = \begin{bmatrix} \boldsymbol{u_x} \\ \boldsymbol{u_y} \\ \boldsymbol{u_z} \end{bmatrix} +# \;\;\;\; \textrm{and} \;\;\;\; \begin{bmatrix} \boldsymbol{w_x} \\ \boldsymbol{w_y} \\ \boldsymbol{w_z} \end{bmatrix} +# +# + +############################################### +# +# Import Packages +# --------------- +# +# Here we import the packages required for this tutorial. +# + +from discretize import TensorMesh, TreeMesh +from discretize.utils import mkvc, refine_tree_xyz +import matplotlib.pyplot as plt +import numpy as np + +# sphinx_gallery_thumbnail_number = 1 + + + +############################################# +# 2D Example +# ---------- +# +# Here we apply the curl operator to a vector that lives +# on the edges of a 2D tensor mesh. We then plot the results. +# The goal is to demonstrate the construction of the +# discrete curl operator. In practise, the curl is only present +# in 3D problems. +# + +# Create a uniform grid +h = np.ones(20) +mesh = TensorMesh([h, h], "CC") + +# Get the curl operator +CURL = mesh.edgeCurl # Curl edges to cell centers (goes to faces in 3D) + +# Evaluate curl of a vector function in x and y +edges_x = mesh.gridEx +edges_y = mesh.gridEy + +wx = (-edges_x[:, 1] / np.sqrt(np.sum(edges_x ** 2, axis=1))) * np.exp( + -(edges_x[:, 0] ** 2 + edges_x[:, 1] ** 2) / 6 ** 2 +) + +wy = (edges_y[:, 0] / np.sqrt(np.sum(edges_y ** 2, axis=1))) * np.exp( + -(edges_y[:, 0] ** 2 + edges_y[:, 1] ** 2) / 6 ** 2 +) + +w = np.r_[wx, wy] +curl_w = CURL * w + +# Plot curl of w +fig = plt.figure(figsize=(11, 5)) + +ax1 = fig.add_subplot(121) +mesh.plotImage( + w, ax=ax1, v_type="E", view="vec", stream_opts={"color": "w", "density": 1.0} +) +ax1.set_title("w at cell edges") + +ax2 = fig.add_subplot(122) +mesh.plotImage(curl_w, ax=ax2) +ax2.set_title("curl of w at cell centers") + +fig.show() + + +############################################# +# Mapping and Dimensions +# ---------------------- +# +# When discretizing and solving differential equations, it is +# natural for vector quantities on cell edges or on cell faces +# The curl operator is generally defined by map from edges to faces; +# although one could theoretically construct a curl operator which +# maps from faces to edges. +# +# Here we construct the curl operator for a tensor mesh and for +# a tree mesh. By plotting the operators on a spy plot, we gain +# an understanding of the dimensions +# of the gradient operator and its structure. +# + +# Create a basic tensor mesh +h = np.ones(20) +tensor_mesh = TensorMesh([h, h, h], "CCC") + +# Create a basic tree mesh +h = np.ones(32) +tree_mesh = TreeMesh([h, h, h], origin="CCC") +xp, yp, zp = np.meshgrid([-8., 8.], [-8., 8.], [-8., 8.]) +xyz = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] +tree_mesh = refine_tree_xyz(tree_mesh, xyz, octree_levels=[1, 1], method="box", finalize=False) +tree_mesh.finalize() + +# Construct curl operators +tensor_curl = tensor_mesh.edge_curl # 3D curl from edges to faces +tree_curl = tree_mesh.edge_curl # 3D curl from edges to faces + +fig = plt.figure(figsize=(9, 4)) + +ax1 = fig.add_axes([0.15, 0.05, 0.35, 0.85]) +ax1.spy(tensor_curl, markersize=0.5) +ax1.set_title("Tensor Mesh Curl") + +ax2 = fig.add_axes([0.65, 0.05, 0.30, 0.85]) +ax2.spy(tree_curl, markersize=0.5) +ax2.set_title("Tree Mesh Curl") + +fig.show() + +# Print some properties +print("Curl on Tensor Mesh:") +print("- Number of faces:", str(tensor_mesh.nF)) +print("- Number of edges:", str(tensor_mesh.nE)) +print("- Dimensions of operator:", str(tensor_mesh.nE), "x", str(tensor_mesh.nF)) +print("- Number of non-zero elements:", str(tensor_curl.nnz)) + +print("Curl on Tree Mesh:") +print("- Number of faces:", str(tree_mesh.nF)) +print("- Number of edges:", str(tree_mesh.nE)) +print("- Dimensions of operator:", str(tree_mesh.nE), "x", str(tree_mesh.nF)) +print("- Number of non-zero elements:", str(tree_curl.nnz)) + + diff --git a/tutorials/operators/README.txt b/tutorials/operators/README.txt index 322b101d0..6d1a4a9d3 100644 --- a/tutorials/operators/README.txt +++ b/tutorials/operators/README.txt @@ -1,25 +1,15 @@ Operators ========= -Numerical solutions to differential equations using the finite volume -method require discrete operators. These include averaging operators -and differential operators. Averaging operators are used when a -variable living on some part of the mesh (e.g. nodes, centers, edges or -faces) must be approximated at other locations. Differential operators -include the gradient, divergence, curl and scalar Laplacian. - -The discrete operators are properties of each mesh class (*tensor mesh*, -*tree mesh*, *curvilinear mesh*). An operator is only constructed when -called. Since each mesh type has a similar API, the operators can be -called using the same syntax. - -To learn about discrete operators, we have provided a set -of tutorials. These tutorials aim to teach the user: - - - how to construct averaging and differential operators from a mesh - - how to apply the discrete operators to discrete variables - - how to impose boundary conditions using differential operators - - how discrete differential operators preserve vector calculus identities +To approximate numerical solutions to partial differential equations using the finite volume method, +we must be able to apply the following mathematical operations to discrete quantities living on mesh: + - Interpolation + - Averaging + - Gradient + - Divergence + - Curl +The following tutorials demonstrate the basic construction these operators and how to +apply them to discrete quantities. diff --git a/tutorials/pde/1_1_poisson_nodal.py b/tutorials/pde/1_1_poisson_nodal.py new file mode 100644 index 000000000..e9b2f2965 --- /dev/null +++ b/tutorials/pde/1_1_poisson_nodal.py @@ -0,0 +1,235 @@ +r""" +2D Poisson Equation with Zero Neumann Condition (Nodal Formulation) +=================================================================== + +Here we use the *discretize* package to approximate the solution to a 2D +Poisson equation with zero Neumann boundary conditions. +For our tutorial, we consider the free-space electrostatic problem +for 2 electric point charges of opposite sign. +This tutorial focusses on: + + - approximating basic types of inner products + - discretization which imposes the boundary conditions naturally + - basic disretization of point sources + +""" + +#################################################### +# 1. Formulating the Problem +# -------------------------- +# +# For this tutorial, we would like to compute the electric potential (:math:`\phi`) +# and electric fields (:math:`\mathbf{e}`) in 2D that result from +# a distribution of point charges in a vacuum. Given the electric permittiviy +# is uniform within the domain and equal to the permittivity of free space, the physics +# are defined by a 2D Poisson equation: +# +# .. math:: +# \nabla^2 \phi = - \frac{\rho}{\epsilon_0} +# +# where :math:`\rho` is the charge density and :math:`\epsilon_0` is +# the permittivity of free space. A more practical +# formulation for applying the finite volume method is to +# define the problem as two fist-order differential equations. +# Starting with Gauss's law and Faraday's law in the case of +# electrostatics (:cite:`griffiths1999`): +# +# .. math:: +# &\nabla \cdot \vec{e} = \frac{\rho}{\epsilon_0} \\ +# &\nabla \times \vec{e} = \boldsymbol{0} \;\;\; \Rightarrow \;\;\; \vec{e} = -\nabla \phi \\ +# &\textrm{s.t.} \;\;\; \frac{\partial \phi}{\partial n} \, \Big |_{\partial \Omega} = - \hat{n} \cdot \vec{e} \, \Big |_{\partial \Omega} = 0 +# +# where the Neumann boundary condition on :math:`\phi` implies no electric flux leaves the system. +# For 2 point charges of equal and opposite sign, the charge density is given by: +# +# .. math:: +# \rho = \rho_0 \big [ \delta ( \boldsymbol{r_+}) - \delta (\boldsymbol{r_-} ) \big ] +# :label: tutorial_eq_1_1_2 +# +# where :math:`\rho_0` is a constant. +# We chose this charge distribution so that we would not violate the boundary conditions. +# Since the total electric charge is zero, the net flux leaving the system is zero. +# + +#################################################### +# 2. Taking Inner Products +# ------------------------ +# +# To solve this problem numerically, we take the inner product +# of each differential equation with an appropriate test function. +# Where :math:`\psi` is a scalar test function and :math:`\vec{u}` is a +# vector test function: +# +# .. math:: +# \int_\Omega \psi (\nabla \cdot \vec{e}) \, dv = \frac{1}{\epsilon_0} \int_\Omega \psi \rho \, dv +# :label: tutorial_eq_1_1_3 +# +# and +# +# .. math:: +# \int_\Omega \vec{u} \cdot \vec{e} \, dv = - \int_\Omega \vec{u} \cdot (\nabla \phi ) \, dv +# :label: tutorial_eq_1_1_4 +# +# In the case of Gauss' law, we have a volume integral containing the Dirac delta function. +# Thus expression :eq:`tutorial_eq_1_1_3` becomes: +# +# .. math:: +# \int_\Omega \psi (\nabla \cdot \vec{e}) \, dv = \frac{1}{\epsilon_0} \psi \, q +# :label: tutorial_eq_1_1_5 +# +# where :math:`q` represents an integrated charge density. +# + +#################################################### +# 3. Discretizing the Inner Products +# ---------------------------------- +# +# Here, we let :math:`\boldsymbol{\phi}` be the discrete representation +# of the electic potential :math:`\phi` on the nodes. Since :math:`\vec{e} = -\nabla \phi`, +# it is natural for the discrete representation of the electric fields :math:`\boldsymbol{e}` +# to live on the edges; allowing the discrete gradient operator to map from nodes to edges. +# +# In tutorials for inner products, we showed how to approximate most classes +# of inner products. Since the electric field is discretized at edges, so much the +# discrete representation of the test function :math:`\boldsymbol{u}`. +# Approximating the inner products in expression :eq:`tutorial_eq_1_1_4` +# according to the finite volume method, we obtain: +# +# .. math:: +# \boldsymbol{u^T M_e \, e} = - \boldsymbol{u^T M_e G \, \phi} +# :label: tutorial_eq_1_1_6 +# +# where +# +# - :math:`\boldsymbol{M_e}` is the inner product matrix at edges +# - :math:`\boldsymbol{G}` is the discrete gradient operator from nodes to edges +# +# Now we approximate the inner products in expression :eq:`tutorial_eq_1_1_5`. +# Since :math:`\boldsymbol{e}` lives on the edges, it would be natural for the +# discrete divergence operator to map from edges to nodes. Unfortunately for nodes +# at the boundary, this would require knowledge of the electric field at edges +# outside our domain. +# +# For the left-hand side of expression :eq:`tutorial_eq_1_1_5`, we must use the +# identity :math:`\psi \nabla \cdot \vec{e} = \nabla \cdot \psi\vec{e} - \vec{e} \cdot \nabla \psi` +# and apply the divergence theorem such that expression :eq:`tutorial_eq_1_1_5` becomes: +# +# .. math:: +# - \int_\Omega \vec{e} \cdot \nabla \psi \, dv + \oint_{\partial \Omega} \psi (\hat{n} \cdot \vec{e}) \, da = \frac{1}{\epsilon_0} \psi \, q +# :label: tutorial_eq_1_1_7 +# +# Since :math:`\hat{n} \cdot \vec{e}` is zero on the boundary, the surface integral is equal to zero. +# We have chosen a discretization where boundary conditions are imposed naturally! +# +# Test function :math:`\psi` and the integrated charge density :math:`q` are defined such that their +# discrete representations :math:`\boldsymbol{\psi}` and :math:`\boldsymbol{q}` must live on the nodes. +# The discrete approximation to expression :eq:`tutorial_eq_1_1_7` is given by: +# +# .. math:: +# - \boldsymbol{\psi^T G^T M_e \, e} = \frac{1}{} \boldsymbol{\psi^T q} +# :label: tutorial_eq_1_1_8 +# +# The easiest way to discretize the source is to let :math:`\boldsymbol{q_i}=\rho_0` at the nearest node to the positive charge and +# let :math:`\boldsymbol{q_i}=-\rho_0` at the nearest node to the negative charge. +# The value is zero for all other nodes. +# + +#################################################### +# 4. Solvable Linear System +# ------------------------- +# +# By combining the discrete representations from expressions +# :eq:`tutorial_eq_1_1_6` and :eq:`tutorial_eq_1_1_8` and factoring like-terms +# we obtain: +# +# .. math:: +# \boldsymbol{G^T M_e G \, \phi} = \frac{1}{\epsilon_0} \boldsymbol{q} +# :label: tutorial_eq_1_1_9 +# +# Let :math:`\boldsymbol{A} = \boldsymbol{G^T M_e G}`. +# The linear system :math:`\boldsymbol{A}` has a single null vector and is not invertible. +# To remedy this, we define a reference potential on the boundary +# by setting :math:`A_{0,0} = 1` and by setting all other values in that row to 0. +# We can now solve for the electric potential on the nodes. +# +# Once the electric potential at nodes has been computed, the electric field on +# the edges can be computed using expression :eq:`tutorial_eq_1_1_6`: +# +# .. math:: +# \boldsymbol{e} = - \boldsymbol{G \, \phi} +# + +############################################### +# 5. Implement Discretize +# ----------------------- + +#%% +# Import the necessary packages for the tutorial. + +from discretize import TensorMesh +from pymatsolver import SolverLU +import matplotlib.pyplot as plt +import matplotlib as mpl +import numpy as np +from discretize.utils import sdiag + +mpl.rcParams.update({'font.size':14}) + +#%% +# Construct a mesh. + +h = np.ones(100) +mesh = TensorMesh([h, h], "CC") + +#%% +# Construct the required discrete operators and inner product matrices. + +G = mesh.nodal_gradient # gradient operator (nodes to edges) +Me = mesh.get_edge_inner_product() # edge inner product matrix + +#%% +# Form the linear system and remove the null-space. + +A = G.T * Me * G +A[0,0] = 1. +A[0, 1:] = 0 + +#%% +# Construct the right-hand side. + +xyn = mesh.nodes +kneg = (xyn[:, 0] == -10) & (xyn[:, 1] == 0) # -ve charge at (-10, 0) +kpos = (xyn[:, 0] == 10) & (xyn[:, 1] == 0) # +ve charge at (10, 0) + +rho = np.zeros(mesh.n_nodes) +rho[kneg] = -1 +rho[kpos] = 1 + +#%% +# Solve for the electric potential on the nodes and the electric field +# on the edges. + +AinvM = SolverLU(A) # Define the inverse of A using direct solver +phi = AinvM * rho # Compute electric potential on nodes +E = - G * phi # Compute electric field on edges + +#%% +# Plot the source term, electric potential and electric field. + +fig = plt.figure(figsize=(12, 4)) + +ax1 = fig.add_subplot(131) +mesh.plotImage(rho, v_type="N", ax=ax1) +ax1.set_title("Charge Density") + +ax2 = fig.add_subplot(132) +mesh.plotImage(phi, v_type="N", ax=ax2) +ax2.set_title("Electric Potential") + +ax3 = fig.add_subplot(133) +mesh.plotImage( + E, ax=ax3, v_type="E", view="vec", stream_opts={"color": "w", "density": 1.0} +) +ax3.set_title("Electric Fields") + +plt.tight_layout() diff --git a/tutorials/pde/1_2_poisson_centers.py b/tutorials/pde/1_2_poisson_centers.py new file mode 100644 index 000000000..d1e3944c8 --- /dev/null +++ b/tutorials/pde/1_2_poisson_centers.py @@ -0,0 +1,241 @@ +r""" +2D Poisson Equation with Zero Neumann Condition (Cell Centered Formulation) +=========================================================================== + +Here we use the *discretize* package to approximate the solution to a 2D +Poisson equation with zero Neumann boundary conditions. +For our tutorial, we consider the free-space electrostatic problem +for 2 electric point charges of opposite sign. +This tutorial focusses on: + + - approximating basic types of inner products + - imposing the boundary condition by approximating a surface integral + - basic disretization of point sources + +""" + +#################################################### +# 1. Formulating the Problem +# -------------------------- +# +# For this tutorial, we would like to compute the electric potential (:math:`\phi`) +# and electric fields (:math:`\mathbf{e}`) in 2D that result from +# a distribution of point charges in a vacuum. Given the electric permittiviy +# is uniform within the domain and equal to the permittivity of free space, the physics +# are defined by a 2D Poisson equation: +# +# .. math:: +# \nabla^2 \phi = - \frac{\rho}{\epsilon_0} +# +# where :math:`\rho` is the charge density and :math:`\epsilon_0` is +# the permittivity of free space. A more practical +# formulation for applying the finite volume method is to +# define the problem as two fist-order differential equations. +# Starting with Gauss's law and Faraday's law in the case of +# electrostatics (:cite:`griffiths1999`): +# +# .. math:: +# &\nabla \cdot \vec{e} = \frac{\rho}{\epsilon_0} \\ +# &\nabla \times \vec{e} = \boldsymbol{0} \;\;\; \Rightarrow \;\;\; \vec{e} = -\nabla \phi \\ +# &\textrm{s.t.} \;\;\; \frac{\partial \phi}{\partial n} \, \Big |_{\partial \Omega} = - \hat{n} \cdot \vec{e} \, \Big |_{\partial \Omega} = 0 +# +# where the Neumann boundary condition on :math:`\phi` implies no electric flux leaves the system. +# For 2 point charges of equal and opposite sign, the charge density is given by: +# +# .. math:: +# \rho = \rho_0 \big [ \delta ( \boldsymbol{r_+}) - \delta (\boldsymbol{r_-} ) \big ] +# :label: tutorial_eq_1_2_1 +# +# where :math:`\rho_0` is a constant. +# We chose this charge distribution so that we would not violate the boundary conditions. +# Since the total electric charge is zero, the net flux leaving the system is zero. +# + +#################################################### +# 2. Taking Inner Products +# ------------------------ +# +# To solve this problem numerically, we take the inner product +# of each differential equation with an appropriate test function. +# Where :math:`\psi` is a scalar test function and :math:`\vec{u}` is a +# vector test function: +# +# .. math:: +# \int_\Omega \psi (\nabla \cdot \vec{e}) \, dv = \frac{1}{\epsilon_0} \int_\Omega \psi \rho \, dv +# :label: tutorial_eq_1_2_3 +# +# and +# +# .. math:: +# \int_\Omega \vec{u} \cdot \vec{e} \, dv = - \int_\Omega \vec{u} \cdot (\nabla \phi ) \, dv +# :label: tutorial_eq_1_2_4 +# +# In the case of Gauss' law, we have a volume integral containing the Dirac delta function. +# Thus expression :eq:`tutorial_eq_1_2_3` becomes: +# +# .. math:: +# \int_\Omega \psi (\nabla \cdot \vec{e}) \, dv = \frac{1}{\epsilon_0} \psi \, q +# :label: tutorial_eq_1_2_5 +# +# where :math:`q` represents an integrated charge density. +# + +#################################################### +# 3. Discretizing the Inner Products +# ---------------------------------- +# +# Here, we let :math:`\boldsymbol{\phi}` be the discrete representation +# of the electic potential :math:`\phi` at cell centers. Since :math:`\vec{e} = -\nabla \phi`, +# it is natural for discrete representation of the electric field :math:`\boldsymbol{e}` +# to live on the faces; implying the discrete divergence operator maps from faces to cell centers. +# +# In tutorials for inner products, we showed how to approximate most classes +# of inner products. Since the numerical divergence of :math:`\boldsymbol{e}` is +# mapped to cell centers, the discrete representation of the test function :math:`\boldsymbol{\psi}` +# and the integrated charge density :math:`\boldsymbol{q}` must live at cell centers. +# +# Approximating the inner products in expression :eq:`tutorial_eq_1_2_5` +# according to the finite volume method, we obtain: +# +# .. math:: +# \boldsymbol{\psi^T M_c D e} = \frac{1}{\epsilon_0} \boldsymbol{\psi^T q} +# :label: tutorial_eq_1_2_10 +# +# where +# +# - :math:`\boldsymbol{M_c}` is the inner product matrix at cell centers, +# - :math:`\boldsymbol{D}` is the discrete divergence operator +# - :math:`\boldsymbol{q}` is a discrete representation for the integrated charge density for each cell +# +# The easiest way to discretize the source is to let :math:`\boldsymbol{q_i}=\rho_0` at the nearest +# cell center to the positive charge and to let :math:`\boldsymbol{q_i}=-\rho_0` at +# the nearest cell center to the negative charge. The value is zero for all other cells. +# +# Now we approximate the inner product in expression :eq:`tutorial_eq_1_2_4`. +# Since :math:`\boldsymbol{\phi}` lives at cell centers, it would be natural for the +# discrete gradient operator to map from centers to faces. Unfortunately for +# boundary faces, this would require knowledge of the electric potential at cell +# centers outside our domain. +# For the right-hand side, we must use the identity +# :math:`\vec{u} \cdot \nabla \phi = \nabla \cdot \phi\vec{u} - \phi \nabla \cdot \vec{u}` +# and apply the divergence theorem such that expression :eq:`tutorial_eq_1_2_4` becomes: +# +# .. math:: +# \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \phi \nabla \cdot \vec{u} \, dv - \oint_{\partial \Omega} \phi \hat{n} \cdot \vec{u} \, da +# :label: tutorial_eq_1_2_11 +# +# According to expression :eq:`tutorial_eq_1_2_1`, +# :math:`- \frac{\partial \phi}{\partial n} = 0` on the boundaries. +# To accurately compute the electric potentials at cell centers, +# we must implement the boundary conditions by approximating the surface integral. +# In this case, expression :eq:`tutorial_eq_1_2_11` is approximated by: +# +# .. math:: +# \boldsymbol{u^T M_f \, e} = \boldsymbol{u^T D^T M_c \, \phi} - \boldsymbol{u^T B \, \phi} = - \boldsymbol{\tilde{G} \, \phi} +# :label: tutorial_eq_1_2_12 +# +# where +# +# - :math:`\boldsymbol{M_c}` is the inner product matrix at cell centers +# - :math:`\boldsymbol{M_f}` is the inner product matrix at faces +# - :math:`\boldsymbol{D}` is the discrete divergence operator +# - :math:`\boldsymbol{B}` is a sparse matrix that imposes the Neumann boundary condition +# - :math:`\boldsymbol{\tilde{G}} = - \boldsymbol{D^T M_c} + \boldsymbol{B}` acts as a modified gradient operator with boundary conditions included +# + +#################################################### +# 4. Solvable Linear System +# ------------------------- +# +# By combining the discrete representations from expressions +# :eq:`tutorial_eq_1_2_10` and :eq:`tutorial_eq_1_2_12` and factoring like-terms +# we obtain: +# +# .. math:: +# - \boldsymbol{M_c D M_f^{-1} \tilde{G} \, \phi} = \frac{1}{\epsilon_0} \boldsymbol{q} +# :label: tutorial_eq_1_2_13 +# +# Once the electric potential at cell centers has been computed, the electric field on +# the faces can be computed using expression :eq:`tutorial_eq_1_2_12`: +# +# .. math:: +# \boldsymbol{e} = - \boldsymbol{M_f^{-1} \tilde{G} \, \phi} +# + +############################################### +# 5. Implement Discretize +# ----------------------- + +#%% +# Import the necessary packages for the tutorial. + +from discretize import TensorMesh +from pymatsolver import SolverLU +import matplotlib.pyplot as plt +import matplotlib as mpl +import numpy as np +from discretize.utils import sdiag + +mpl.rcParams.update({'font.size':14}) + +#%% +# Construct a mesh. + +h = 2*np.ones(51) +mesh = TensorMesh([h, h], "CC") + +#%% +# Construct the necessary discrete operators and inner product matrices. + +DIV = mesh.faceDiv # discrete divergence operator +Mc = sdiag(mesh.vol) # cell center inner product matrix +Mf_inv = mesh.get_face_inner_product(invert_matrix=True) # inverse of face inner product matrix + +mesh.set_cell_gradient_BC(['neumann','neumann']) # Set zero Neumann condition on gradient +G = mesh.cell_gradient # Modified gradient operator G = -D^T * Mc + B + +#%% +# Form the linear system of equations to be solved. + +A = - Mc * DIV * Mf_inv * G + +#%% +# Construct the right-hand side. + +xycc = mesh.gridCC +kneg = (xycc[:, 0] == -10) & (xycc[:, 1] == 0) # -ve charge at (-10, 0) +kpos = (xycc[:, 0] == 10) & (xycc[:, 1] == 0) # +ve charge at (10, 0) + +rho = np.zeros(mesh.nC) +rho[kneg] = -1 +rho[kpos] = 1 + +#%% +# Solve for the electric potential at cell centers and the electric field +# on the faces. + +AinvM = SolverLU(A) # Define the inverse of A using direct solver +phi = AinvM * rho # Compute electric potential at cell centers +E = - Mf_inv * G * phi # Compute electric field on faces + +#%% +# Plot the source term, electric potential and electric field. + +fig = plt.figure(figsize=(12, 4)) + +ax1 = fig.add_subplot(131) +mesh.plotImage(rho, v_type="CC", ax=ax1) +ax1.set_title("Charge Density") + +ax2 = fig.add_subplot(132) +mesh.plotImage(phi, v_type="CC", ax=ax2) +ax2.set_title("Electric Potential") + +ax3 = fig.add_subplot(133) +mesh.plotImage( + E, ax=ax3, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} +) +ax3.set_title("Electric Fields") + +plt.tight_layout() + diff --git a/tutorials/pde/1_poisson.py b/tutorials/pde/1_poisson.py deleted file mode 100644 index 4919ff3db..000000000 --- a/tutorials/pde/1_poisson.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -Gauss' Law of Electrostatics -============================ - -Here we use the discretize package to solve for the electric potential -(:math:`\phi`) and electric fields (:math:`\mathbf{e}`) in 2D that result from -a static charge distribution. Starting with Gauss' law and Faraday's law: - -.. math:: - &\\nabla \\cdot \mathbf{E} = \\frac{\\rho}{\\epsilon_0} \n - &\\nabla \\times \mathbf{E} = \\mathbf{0} \;\;\; \Rightarrow \;\;\; \\mathbf{E} = -\\nabla \\phi \n - &\\textrm{s.t.} \;\;\; \phi \Big |_{\partial \Omega} = 0 - -where :math:`\\sigma` is the charge density and :math:`\\epsilon_0` is the -permittivity of free space. We will consider the case where there is both a -positive and a negative charge of equal magnitude within our domain. Thus: - -.. math:: - \\rho = \\rho_0 \\big [ \\delta ( \\mathbf{r_+}) - \\delta (\\mathbf{r_-} ) \\big ] - -To solve this problem numerically, we use the weak formulation; that is, we -take the inner product of each equation with an appropriate test function. -Where :math:`\\psi` is a scalar test function and :math:`\\mathbf{f}` is a -vector test function: - -.. math:: - \\int_\\Omega \\psi (\\nabla \\cdot \\mathbf{E}) dV = \\frac{1}{\\epsilon_0} \\int_\\Omega \\psi \\rho dV \n - \\int_\\Omega \\mathbf{f \\cdot E} \\, dV = - \\int_\\Omega \\mathbf{f} \\cdot (\\nabla \\phi ) dV - - -In the case of Gauss' law, we have a volume integral containing the Dirac delta -function, thus: - -.. math:: - \\int_\\Omega \\psi (\\nabla \\cdot \\mathbf{E}) dV = \\frac{1}{\\epsilon_0} \\psi \\, q - -where :math:`q` represents an integrated charge density. By applying the finite -volume approach to this expression we obtain: - -.. math:: - \\mathbf{\\psi^T M_c D e} = \\frac{1}{\\epsilon_0} \\mathbf{\\psi^T q} - -where :math:`\mathbf{q}` denotes the total enclosed charge for each cell. Thus -:math:`\mathbf{q_i}=\\rho_0` for the cell containing the positive charge and -:math:`\mathbf{q_i}=-\\rho_0` for the cell containing the negative charge. It -is zero for every other cell. - -:math:`\mathbf{\psi}` and :math:`\mathbf{q}` live at cell centers and -:math:`\mathbf{e}` lives on cell faces. :math:`\mathbf{D}` is the discrete -divergence operator. :math:`\\mathbf{M_c}` is an inner product matrix for cell -centered quantities. - -For the second weak form equation, we make use of the divergence theorem as -follows: - -.. math:: - \\int_\\Omega \\mathbf{f \\cdot E} \\, dV &= - \\int_\\Omega \\mathbf{f} \\cdot (\\nabla \\phi ) dV \n - & = - \\frac{1}{\\epsilon_0} \\int_\\Omega \\nabla \\cdot (\\mathbf{f} \\phi ) dV + \\frac{1}{\\epsilon_0} \\int_\\Omega ( \\nabla \\cdot \\mathbf{f} ) \\phi \\, dV \n - & = - \\frac{1}{\\epsilon_0} \\int_{\\partial \\Omega} \\mathbf{n} \\cdot (\\mathbf{f} \\phi ) da + \\frac{1}{\\epsilon_0} \\int_\\Omega ( \\nabla \\cdot \\mathbf{f} ) \\phi \\, dV \n - & = 0 + \\frac{1}{\\epsilon_0} \\int_\\Omega ( \\nabla \\cdot \\mathbf{f} ) \\phi \\, dV - -where the surface integral is zero due to the boundary conditions we imposed. -Evaluating this expression according to the finite volume approach we obtain: - -.. math:: - \\mathbf{f^T M_f e} = \\mathbf{f^T D^T M_c \\phi} - -where :math:`\\mathbf{f}` lives on cell faces and :math:`\\mathbf{M_f}` is the -inner product matrix for quantities that live on cell faces. By canceling terms -and combining the set of discrete equations we obtain: - -.. math:: - \\big [ \\mathbf{M_c D M_f^{-1} D^T M_c} \\big ] \\mathbf{\\phi} = \\frac{1}{\epsilon_0} \mathbf{q} - -from which we can solve for :math:`\mathbf{\phi}`. The electric field can be -obtained by computing: - -.. math:: - \mathbf{e} = \\mathbf{M_f^{-1} D^T M_c \\phi} - - - -""" - -############################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial. -# - - -from discretize import TensorMesh -from pymatsolver import SolverLU -import matplotlib.pyplot as plt -import numpy as np -from discretize.utils import sdiag - - -############################################### -# -# Solving the Problem -# ------------------- -# - -# Create a tensor mesh -h = np.ones(75) -mesh = TensorMesh([h, h], "CC") - -# Create system -DIV = mesh.faceDiv # Faces to cell centers divergence -Mf_inv = mesh.getFaceInnerProduct(invMat=True) -Mc = sdiag(mesh.vol) -A = Mc * DIV * Mf_inv * DIV.T * Mc - -# Define RHS (charge distributions at cell centers) -xycc = mesh.gridCC -kneg = (xycc[:, 0] == -10) & (xycc[:, 1] == 0) # -ve charge distr. at (-10, 0) -kpos = (xycc[:, 0] == 10) & (xycc[:, 1] == 0) # +ve charge distr. at (10, 0) - -rho = np.zeros(mesh.nC) -rho[kneg] = -1 -rho[kpos] = 1 - -# LU factorization and solve -AinvM = SolverLU(A) -phi = AinvM * rho - -# Compute electric fields -E = Mf_inv * DIV.T * Mc * phi - -# Plotting -fig = plt.figure(figsize=(14, 4)) - -ax1 = fig.add_subplot(131) -mesh.plotImage(rho, v_type="CC", ax=ax1) -ax1.set_title("Charge Density") - -ax2 = fig.add_subplot(132) -mesh.plotImage(phi, v_type="CC", ax=ax2) -ax2.set_title("Electric Potential") - -ax3 = fig.add_subplot(133) -mesh.plotImage( - E, ax=ax3, v_type="F", view="vec", stream_opts={"color": "w", "density": 1.0} -) -ax3.set_title("Electric Fields") diff --git a/tutorials/pde/2_advection_diffusion.py b/tutorials/pde/2_advection_diffusion.py index d32478f1a..4fc3b35f4 100644 --- a/tutorials/pde/2_advection_diffusion.py +++ b/tutorials/pde/2_advection_diffusion.py @@ -1,188 +1,257 @@ -""" -Advection-Diffusion Equation -============================ - -Here we use the discretize package to model the advection-diffusion -equation. The goal of this tutorial is to demonstrate: - - - How to solve time-dependent PDEs - - How to apply Neumann boundary conditions - - Strategies for applying finite volume to 2nd order PDEs - - -Derivation ----------- - -If we assume the fluid is incompressible (:math:`\\nabla \\cdot \\mathbf{u} = 0`), -the advection-diffusion equation with Neumann boundary conditions is given by: - -.. math:: - p_t = \\nabla \\cdot \\alpha \\nabla p - - \\mathbf{u} \\cdot \\nabla p + s \n - \\textrm{s.t.} \;\;\; \\frac{\\partial p}{\\partial n} \Bigg |_{\partial \Omega} = 0 - -where :math:`p` is the unknown variable, :math:`\\alpha` defines the -diffusivity within the domain, :math:`\\mathbf{u}` is the velocity field, and -:math:`s` is the source term. We will consider the case where there is a single -point source within our domain. Thus: - -.. math:: - s = s_0 \\delta ( \\mathbf{r} ) - -where :math:`s_0` is a constant. To solve this problem numerically, we -re-express the advection-diffusion equation as a set of first order PDEs: - -.. math:: - \\; \\; p_t = \\nabla \\cdot \\mathbf{j} - \\mathbf{u} \\cdot \\mathbf{w} + s \\;\\;\\; (1)\n - \\; \\; \\mathbf{w} = \\nabla p \\;\\;\\; (2) \n - \\; \\; \\alpha^{-1} \\mathbf{j} = \\mathbf{w} \\;\\;\\; (3) - - -We then apply the weak formulation; that is, we -take the inner product of each equation with an appropriate test function. - -**Expression 1:** - -Let :math:`\\psi` be a scalar test function. By taking the inner product with -expression (1) we obtain: - -.. math:: - \int_\\Omega \\psi \\, p_t \\, dv = - \int_\\Omega \\psi \\, (\\nabla \\cdot \\mathbf{j}) \\, dv - - \int_\\Omega \\psi \\, \\big ( \mathbf{u} \\cdot \\mathbf{w} \\big ) \\, dv - + s_0 \int_\\Omega \\psi \\, \\delta (\\mathbf{r}) \\, dv - -The source term is a volume integral containing the Dirac delta function, thus: - -.. math:: - s_0 \int_\\Omega \\psi \\, \\delta (\\mathbf{r}) \\, dv \\approx \\mathbf{\\psi^T \\, q} - -where :math:`q=s_0` for the cell containing the point source and zero everywhere -else. By evaluating the inner products according to the finite volume approach -we obtain: - -.. math:: - \\mathbf{\\psi^T M_c p_t} = \\mathbf{\\psi^T M_c D \\, j} - - \\mathbf{\\psi^T M_c A_{fc}} \\, \\textrm{diag} ( \\mathbf{u} ) \\mathbf{w} - + \\mathbf{\\psi^T q} - -where :math:`\\mathbf{\\psi}`, :math:`\\mathbf{p}` and :math:`\\mathbf{p_t}` -live at cell centers and :math:`\\mathbf{j}`, :math:`\\mathbf{u}` and -:math:`\\mathbf{w}` live on cell faces. :math:`\\mathbf{D}` -is a discrete divergence operator. :math:`\\mathbf{M_c}` is the cell center -inner product matrix. :math:`\\mathbf{A_{fc}}` takes the dot product of -:math:`\\mathbf{u}` and :math:`\\mathbf{w}`, projects it to cell centers and sums -the contributions by each Cartesian component. - -**Expression 2:** - -Let :math:`\\mathbf{f}` be a vector test function. By taking the inner product -with expression (2) we obtain: +r""" +2D Advection-Diffusion Equation with Zero Neumann Condition +=========================================================== -.. math:: - \\int_\\Omega \\mathbf{f \\cdot w} \\, dv = - \\int_\\Omega \\mathbf{f \\cdot \\nabla}p \\, dv +Here we use the *discretize package* to approximate the solution to +the 2D advection-diffusion equation with zero Neumann boundary conditions. +We assume the fluid is incompressible. +This tutorial focusses on: -If we use the identity -:math:`\\phi \\nabla \\cdot \\mathbf{a} = \\nabla \\cdot (\\phi \\mathbf{a}) - \\mathbf{a} \\cdot (\\nabla \\phi )` -and apply the divergence theorem we obtain: + - strategies for applying the finite volume method to higher order PDEs + - discretizing and solving time-dependent PDEs + - including constitutive relationships defined by the reciprocal of a parameter + -.. math:: - \\int_\\Omega \\mathbf{f \\cdot w} \\, dv = - \\int_{\\partial \\Omega} \\mathbf{n \\, \\cdot} \\, p \\mathbf{f} \\, da - - \\int_{\\Omega} p \\cdot \\nabla \\mathbf{f} \\, dv - -If we assume that :math:`f=0` on the boundary, we can eliminate the surface -integral. By evaluating the inner products in the weak formulation according -to the finite volume approach we obtain: - -.. math:: - \\mathbf{f^T M_f w} = - \\mathbf{f^T D^T M_c}p - -where :math:`\\mathbf{f}` lives at cell faces and :math:`\\mathbf{M_f}` is -the face inner product matrix. - -**Expression 3:** - -Let :math:`\\mathbf{f}` be a vector test function. By taking the inner product -with expression (3) we obtain: - -.. math:: - \\int_\\Omega \\mathbf{f} \\cdot \\alpha^{-1} \\mathbf{j} \\, dv = - \\int_\\Omega \\mathbf{f} \\cdot \\mathbf{w} \\, dv - -By evaluating the inner products according to the finite volume approach -we obtain: - -.. math:: - \\mathbf{f^T M_\\alpha \\, j} = \\mathbf{f^T M_f \\, w} - -where :math:`\\mathbf{M_\\alpha}` is a face inner product matrix that -depends on the inverse of the diffusivity. - -**Final Numerical System:** - -By combining the set of discrete expressions and letting -:math:`\\mathbf{s} = \\mathbf{M_c^{-1} q}`, we obtain: - -.. math:: - \\mathbf{p_t} = - - \\mathbf{D M_\\alpha^{-1} \\, D^T \\, M_c} \\, p - + \\mathbf{A_{fc}} \\, \\textrm{diag} ( \\mathbf{u} ) \\mathbf{M_f^{-1} D^T \\, M_c} p - + \\mathbf{s} - -Since the Neumann boundary condition is being used for the variable :math:`p`, -the transpose of the divergence operator is the negative of the gradient -operator with Neumann boundary conditions; e.g. :math:`\\mathbf{D^T = -G}`. Thus: - -.. math:: - \\mathbf{p_t} = - \\mathbf{M} \\mathbf{p} + \\mathbf{s} - -where - -.. math:: - \\mathbf{M} = - \\mathbf{D M_\\alpha^{-1} \\, G \\, M_c} \\, p - + \\mathbf{A_{fc}} \\, \\textrm{diag} ( \\mathbf{u} ) \\mathbf{M_f^{-1} G \\, M_c} p +""" -For the example, we will discretize in time using backward Euler. This results -in the following system which must be solve at every time step :math:`k`. -Where :math:`\\Delta t` is the step size: +#################################################### +# 1. Formulating the Problem +# -------------------------- +# +# If we assume the fluid is incompressible (i.e. :math:`\nabla \cdot \vec{u} = 0`), +# the advection-diffusion equation with zero Neumann boundary conditions is given by: +# +# .. math:: +# \begin{align} +# & p_t = \nabla \cdot \alpha \nabla p - \vec{u} \cdot \nabla p + s \\ +# & \textrm{s.t.} \;\;\; \frac{\partial p}{\partial n} \Bigg |_{\partial \Omega} = 0 \\ +# & \textrm{and} \;\;\; p(t=0) = 0 +# \end{align} +# :label: tutorial_eq_2_1_1 +# +# where +# +# - :math:`p` is the unknown variable +# - :math:`p_t` is its time-derivative +# - :math:`\alpha` defines the diffusivity within the domain +# - :math:`\vec{u}` is the velocity field +# - :math:`s` is the source term +# +# We will consider the case where there is a single point source within our domain. +# Where :math:`s_0` is a constant: +# +# .. math:: +# s = s_0 \delta ( \vec{r} ) +# +# Direct implementation of the finite volume method is more challenging for higher order PDEs. +# One strategy is to redefine the problem as a set of first order PDEs: +# +# .. math:: +# \begin{align} +# p_t = \nabla \cdot \vec{j} - \vec{u} \cdot \vec{w} + s \;\;\;\; &(1)\\ +# \vec{w} = \nabla p \;\;\;\; &(2)\\ +# \alpha^{-1} \vec{j} = \vec{w} \;\;\;\; &(3) +# \end{align} +# :label: tutorial_eq_2_1_2 +# +# We can now take the inner products between each expression in equation +# :eq:`tutorial_eq_2_1_2` and an appropriate test function. +# -.. math:: - \\big [ \\mathbf{I} + \\Delta t \\, \\mathbf{M} \\big ] \\mathbf{p}^{k+1} = - \\mathbf{p}^k + \\Delta t \\, \\mathbf{s} +#################################################### +# 2. Taking Inner Products +# ------------------------ +# +# The inner product between a scalar test function :math:`\psi` and the first equation +# in :eq:`tutorial_eq_2_1_2` is given by: +# +# .. math:: +# \int_\Omega \psi p_t \, dv = \int_\Omega \psi \nabla \cdot \vec{j} \, dv + \int_\Omega \psi (\vec{u} \cdot \vec{w}) \, dv + \int_\Omega \psi s \, dv +# :label: tutorial_eq_2_1_3 +# +# The inner product between a vector test function :math:`\vec{f}` and the second equation +# in :eq:`tutorial_eq_2_1_2` is given by: +# +# .. math:: +# \int_\Omega \vec{f} \cdot \vec{w} = \int_\Omega \vec{f} \cdot \nabla p \, dv +# :label: tutorial_eq_2_1_4 +# +# And the inner product between a vector test function :math:`\vec{f}` and the third equation +# in :eq:`tutorial_eq_2_1_2` is given by: +# +# .. math:: +# \int_\Omega \vec{f} \cdot \alpha^{\! -1} \vec{j} \, dv = \int_\Omega \vec{f} \cdot \vec{w} \, dv +# :label: tutorial_eq_2_1_5 +# + +#################################################### +# 3. Discretizing the Inner Products +# ---------------------------------- +# +# Because this is a time-dependent problem, we must consider discretization in both space and time. +# We generally begin by discretizing in space, then we discretize in time. +# Here we let :math:`\boldsymbol{p}` be the discrete representation of the unkown variable :math:`p` and its time-derivative :math:`p_t` +# at cell centers. Examining expressions :eq:`tutorial_eq_2_1_3`, +# :eq:`tutorial_eq_2_1_4` and :eq:`tutorial_eq_2_1_5`: +# +# - The scalar test function :math:`\psi` must be discretized to cell centers +# - The source term :math:`s` must also be discretized to cell centers +# - For this discretization, the divergence operator maps naturally from faces to cell centers and :math:`\vec{j}` must be discretized to faces +# - For this discretization, the gradient operator maps naturally from cell centers to faces and :math:`\vec{w}` must be discretized to faces +# - Since :math:`\vec{w}` is discretized to the faces, so must the vector field :math:`\vec{u}` and the vector test function :math:`\vec{f}` +# +# **Inner Product #1:** +# +# Since the source term in expression :eq:`tutorial_eq_2_1_3` contains a Dirac delta function, +# we rewrite the expression as: +# +# .. math:: +# \int_\Omega \psi p_t \, dv = \int_\Omega \psi \nabla \cdot \vec{j} \, dv + \int_\Omega \psi (\vec{u} \cdot \vec{w}) \, dv + \psi q +# :label: tutorial_eq_2_1_6 +# +# where :math:`q` is an integrated source term. Here, discrete representations of scalars (:math:`\boldsymbol{\psi}`, :math:`\boldsymbol{p_t}` and :math:`\boldsymbol{q}`) +# will live at cell centers, while discrete representations of vectors (:math:`\boldsymbol{j}`, :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}`) +# will live on the faces. +# +# In the tutorials for inner products, we showed how to approximate most classes of inner products. +# However the third term in expression :eq:`tutorial_eq_2_1_6` is challenging. +# The inner product for this term is approximated using discrete scalar quantities at cell centers, +# but discrete vectors :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` live on the faces. +# To remedy this, we multiply the Cartesian components of :math:`\boldsymbol{u}` and :math:`\boldsymbol{w}` +# independently on their respective faces, then project the quantity to the cell centers +# before carrying out the dot product. Our final discretized representation of +# expression :eq:`tutorial_eq_2_1_6` is as follows: +# +# .. math:: +# \boldsymbol{\psi^T M_c \, p_t} = \boldsymbol{\psi^T M_c D \, j} - +# c\, \boldsymbol{\psi^T M_c A_{fc}} \, diag(\boldsymbol{u}) \, \boldsymbol{w} + \boldsymbol{\psi^T q} +# :label: tutorial_eq_2_1_7 +# +# where +# +# - :math:`\boldsymbol{M_c}` is the inner product matrix at cell centers +# - :math:`\boldsymbol{D}` is the discrete divergence operator from faces to cell centers +# - :math:`\boldsymbol{A_{fc}}` is a projection matrix from faces to cell centers +# - :math:`c=1,2,3` is a constant equal to the dimension of the problem (*c* = 2 in this case) +# +# **Inner Product #2:** +# +# Consider expression :eq:`tutorial_eq_2_1_4`. +# The discrete gradient operator maps naturally from cell centers to faces. +# Unfortunately for boundary faces, this would require we know values of :math:`p` outside the domain. +# By using the vector identity :math:`\vec{f} \cdot \nabla p = \nabla \cdot p\vec{f} - p \nabla \cdot \vec{f}` +# and applying the divergence theorem, expression :eq:`tutorial_eq_2_1_4` becomes: +# +# .. math:: +# \int_\Omega \vec{f} \cdot \vec{w} = - \int_\Omega p \nabla \cdot \vec{f} \, dv + \oint_{\partial \Omega} p \hat{n} \cdot \vec{f} \, da +# :label: tutorial_eq_2_1_8 +# +# The discrete approximation is therefore given by: +# +# .. math:: +# \boldsymbol{f^T M_f \, w} = - \boldsymbol{f^T D^T M_c \, p + f^T B \, p} = \boldsymbol{f^T \tilde{G} \, p} +# :label: tutorial_eq_2_1_9 +# +# where +# +# - :math:`\boldsymbol{f}` is the discrete repressentation of :math:`\vec{f}` on cell faces +# - :math:`\boldsymbol{M_f}` is the inner product matrix on cell faces +# - :math:`\boldsymbol{B}` is a sparse matrix that imposes boundary conditions correctly on :math:`p` +# - :math:`\boldsymbol{\tilde{G}} = \boldsymbol{-D^T M_c + B}` acts as a modified gradient operator with boundary conditions implemented +# +# **Inner Product #3:** +# +# In expression :eq:`tutorial_eq_2_1_5`, we must take the inner product +# where the constitutive relation is defined by the reciprocal of a parameter. +# This was covered in the inner products section for constitutive relationships. +# The resulting approximation to expression :eq:`tutorial_eq_2_1_5` +# is given by: +# +# .. math:: +# \boldsymbol{f^T M_\alpha \, j} = \boldsymbol{f^T M_f w} +# :label: tutorial_eq_2_1_10 +# +# where :math:`\boldsymbol{M_\alpha}` is the inner product matrix at faces +# for the reciprocal of the diffusivity. +# +#################################################### +# 4. Solvable Linear System +# ------------------------- +# +# Before we can solve the problem numerically, we must amalgamate our +# discrete expressions and discretize in time. +# We begin by substituting the discrete representations of the inner products from expressions +# :eq:`tutorial_eq_2_1_8` and :eq:`tutorial_eq_2_1_10` +# into expression :eq:`tutorial_eq_2_1_6` and factoring like-terms. +# The resulting system of equations discretized in space is given by: +# +# .. math:: +# \boldsymbol{p_t} = \boldsymbol{\big [ D \, M_\alpha^{-1} \tilde{G}} - +# c\, \boldsymbol{A_{fc}} diag(\boldsymbol{u}) \, \boldsymbol{M_f^{-1} \tilde{G} \big ] \, p} + \boldsymbol{M_c^{-1} \, q} +# :label: tutorial_eq_2_1_11 +# +# To discretize in time, let us re-express equations :eq:`tutorial_eq_2_1_11` as: +# +# .. math:: +# \boldsymbol{p_t} = \boldsymbol{- M \, p + s} +# :label: tutorial_eq_2_1_12 +# +# where +# +# .. math:: +# \boldsymbol{M} = - \boldsymbol{D \, M_\alpha^{-1} \tilde{G}} + +# c\, \boldsymbol{A_{fc}} diag(\boldsymbol{u}) \, \boldsymbol{M_f^{-1} \tilde{G}} +# :label: tutorial_eq_2_1_13 +# +# and +# +# .. math:: +# \boldsymbol{s} = \boldsymbol{M_c^{-1} \, q} +# :label: tutorial_eq_2_1_14 +# +# There are a multitude of ways in which discretization in time can be implemented. +# A stable and easy method to implement is the backward Euler. +# By implementing the backward Euler, we must solve the following linear system +# at each time step :math:`k`: +# +# .. math:: +# \big [ \boldsymbol{I} + \Delta t \, \boldsymbol{M} \big ] \, \boldsymbol{p}^{k+1} = \boldsymbol{p}^k + \Delta t \, \boldsymbol{s} +# :label: tutorial_eq_2_1_15 +# +# where :math:`\boldsymbol{I}` is the identity matrix and :math:`\Delta t` is the step length. +# +# -""" -################################################### -# -# Import Packages -# --------------- -# -# Here we import the packages required for this tutorial. +############################################### +# 5. Solving the System with Discretize +# ------------------------------------- # +#%% +# Import the necessary packages for the tutorial. + from discretize import TensorMesh from pymatsolver import SolverLU import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np from discretize.utils import sdiag, mkvc +mpl.rcParams.update({'font.size':14}) +# sphinx_gallery_thumbnail_number = 2 -############################################### -# -# Solving the Problem -# ------------------- -# +#%% +# Construct a mesh. -# Create a tensor mesh h = np.ones(75) mesh = TensorMesh([h, h], "CC") -# Define a divergence free vector field on faces -faces_x = mesh.gridFx -faces_y = mesh.gridFy +#%% +# Define a divergence free vector field on the faces and plot. + +faces_x = mesh.faces_x +faces_y = mesh.faces_y r_x = np.sqrt(np.sum(faces_x ** 2, axis=1)) r_y = np.sqrt(np.sum(faces_y ** 2, axis=1)) @@ -190,74 +259,92 @@ ux = 0.5 * (-faces_x[:, 1] / r_x) * (1 + np.tanh(0.15 * (28.0 - r_x))) uy = 0.5 * (faces_y[:, 0] / r_y) * (1 + np.tanh(0.15 * (28.0 - r_y))) -u = 10.0 * np.r_[ux, uy] # Maximum velocity is 10 m/s +u_max = 10.0 # Maximum velocity is 10 m/s +u = u_max * np.r_[ux, uy] -# Define vector q where s0 = 1 in our analytic source term -xycc = mesh.gridCC -k = (xycc[:, 0] == 0) & (xycc[:, 1] == -15) # source at (0, -15) +fig = plt.figure(figsize=(8, 6)) +ax = 2 * [None] + +ax[0] = fig.add_axes([0.15, 0.1, 0.6, 0.8]) +mesh.plotImage( + u, + ax=ax[0], + v_type="F", + view="vec", + stream_opts={"color": "w", "density": 1.0}, + clim=[0.0, 10.0], +) +ax[0].set_title("Divergence free vector field") + +ax[1] = fig.add_axes([0.8, 0.1, 0.05, 0.8]) +ax[1].set_aspect(10, anchor="W") +norm = mpl.colors.Normalize(vmin=0, vmax=u_max) +cbar = mpl.colorbar.ColorbarBase(ax[1], norm=norm, orientation="vertical") +cbar.set_label("Velocity (m/s)", rotation=270, labelpad=15) +#%% +# Construct the source term. Her we define a discrete vector q where +# qi=1 at the nearest cell center and zero for all other cells. + +xycc = mesh.cell_centers +k = (xycc[:, 0] == 0) & (xycc[:, 1] == -15) # source at (0, -15) q = np.zeros(mesh.nC) q[k] = 1 -# Define diffusivity within each cell -a = mkvc(8.0 * np.ones(mesh.nC)) +#%% +# Define the diffusivity for all cells. In this case the diffusivity +# is the same for all cells. However, a more complex distribution +# of diffusivities could be created here. -# Define the matrix M -Afc = mesh.dim * mesh.aveF2CC # modified averaging operator to sum dot product -Mf_inv = mesh.getFaceInnerProduct(invMat=True) -Mc = sdiag(mesh.vol) -Mc_inv = sdiag(1 / mesh.vol) -Mf_alpha_inv = mesh.getFaceInnerProduct(a, invProp=True, invMat=True) - -mesh.setCellGradBC(["neumann", "neumann"]) # Set Neumann BC -G = mesh.cellGrad -D = mesh.faceDiv +a = mkvc(8.0 * np.ones(mesh.nC)) -M = -D * Mf_alpha_inv * G * Mc + Afc * sdiag(u) * Mf_inv * G * Mc +#%% +# Define any discrete operators and inner product matrices require to +# solve the problem. +Afc = mesh.average_face_to_cell # average face to cell matrix +Mf_inv = mesh.getFaceInnerProduct(invert_matrix=True) # inverse of inner product matrix at faces +Mc = sdiag(mesh.vol) # inner product matrix at centers +Mc_inv = sdiag(1 / mesh.vol) # inverse of inner product matrix at centers +Mf_alpha_inv = mesh.getFaceInnerProduct( + a, invert_model=True, invert_matrix=True +) # Inverse of the inner product matrix for the reciprocal of the diffusivity -# Set time stepping, initial conditions and final matricies -dt = 0.02 # Step width -p = np.zeros(mesh.nC) # Initial conditions p(t=0)=0 +D = mesh.face_divergence # divergence operator -I = sdiag(np.ones(mesh.nC)) # Identity matrix -B = I + dt * M -s = Mc_inv * q +mesh.set_cell_gradient_BC(["neumann", "neumann"]) # Set zero Neumann BC +G = mesh.cell_gradient # modified gradient operator with BC implemented -Binv = SolverLU(B) +#%% +# Construct the linear system that is solved at each time step. +M = -D * Mf_alpha_inv * G * Mc + mesh.dim * Afc * sdiag(u) * Mf_inv * G * Mc -# Plot the vector field -fig = plt.figure(figsize=(15, 15)) -ax = 9 * [None] +dt = 0.02 # Step width +p = np.zeros(mesh.nC) # Initial conditions p(t=0)=0 -ax[0] = fig.add_subplot(332) -mesh.plotImage( - u, - ax=ax[0], - v_type="F", - view="vec", - stream_opts={"color": "w", "density": 1.0}, - clim=[0.0, 10.0], -) -ax[0].set_title("Divergence free vector field") +I = sdiag(np.ones(mesh.nC)) # Identity matrix +B = I + dt * M # Linear system solved at each time step +s = Mc_inv * q # RHS -ax[1] = fig.add_subplot(333) -ax[1].set_aspect(10, anchor="W") -cbar = mpl.colorbar.ColorbarBase(ax[1], orientation="vertical") -cbar.set_label("Velocity (m/s)", rotation=270, labelpad=5) +Binv = SolverLU(B) # Define inverse of B using solver -# Perform backward Euler and plot +#%% +# Perform backward Euler at each time step and plot at specified times. -n = 3 +fig = plt.figure(figsize=(15, 10)) +ax = 6 * [None] +n = 0 for ii in range(300): p = Binv * (p + s) if ii + 1 in (1, 25, 50, 100, 200, 300): - ax[n] = fig.add_subplot(3, 3, n + 1) + ax[n] = fig.add_subplot(2, 3, n+1) mesh.plotImage(p, v_type="CC", ax=ax[n], pcolor_opts={"cmap": "gist_heat_r"}) title_str = "p at t = " + str((ii + 1) * dt) + " s" ax[n].set_title(title_str) n = n + 1 + +plt.tight_layout() \ No newline at end of file diff --git a/tutorials/pde/README.txt b/tutorials/pde/README.txt index a28420b90..0f07afe51 100644 --- a/tutorials/pde/README.txt +++ b/tutorials/pde/README.txt @@ -1,11 +1,12 @@ -Solving PDEs -============ +Solving PDEs with Discretize +============================ Here we show how the *discretize* package can be used to solve partial differential -equations (PDE) numerically by employing the finite volume method. To solve a PDE -numerically we must complete the following steps: +equations (PDEs) numerically by employing the finite volume method. In each tutorial, +we demonstrate the following steps for a given PDE: - 1. Formulate the problem; e.g. the PDE and its boundary conditions - 2. Apply the weak formulation by taking the inner product of each PDE with a test function - 3. Formulate a discrete set of equations for the inner products according to the finite volume method - 4. Use the discrete set of equations to solve for the unknown variable numerically + 1. Formulating the problem; i.e. the PDE and its boundary conditions + 2. Taking the inner product of each differential expression + 3. Approximating the inner products as discrete expressions according to the finite volume method + 4. Reducing the set of discrete expressions to a solvable linear system + 5. Implementing the *discretize* package to construct the necessary mesh, operators, matrices, etc... and solve the system