Skip to content

Commit

Permalink
Merge pull request #767 from pybamm-team/issue-765-compute-grad-fem
Browse files Browse the repository at this point in the history
Issue 765 compute grad fem
  • Loading branch information
rtimms authored Dec 28, 2019
2 parents c981057 + dc2543f commit 3098ec6
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 22 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- Added the gradient operation for the Finite Element Method ([#767](https://github.com/pybamm-team/PyBaMM/pull/767))
- Added `InputParameter` node for quickly changing parameter values ([#752](https://github.com/pybamm-team/PyBaMM/pull/752))
- Added submodels for operating modes other than current-controlled ([#751](https://github.com/pybamm-team/PyBaMM/pull/751))
- Changed finite volume discretisation to use exact values provided by Neumann boundary conditions when computing the gradient instead of adding ghost nodes([#748](https://github.com/pybamm-team/PyBaMM/pull/748))
Expand Down Expand Up @@ -35,6 +36,7 @@

## Bug fixes

- Fixed a bug which meant that the Ohmic heating in the current collectors was incorrect if using the Finite Element Method ([#767](https://github.com/pybamm-team/PyBaMM/pull/767))
- Improved automatic broadcasting ([#747](https://github.com/pybamm-team/PyBaMM/pull/747))
- Fixed bug with wrong temperature in initial conditions ([#737](https://github.com/pybamm-team/PyBaMM/pull/737))
- Improved flexibility of parameter values so that parameters (such as diffusivity or current) can be set as functions or scalars ([#723](https://github.com/pybamm-team/PyBaMM/pull/723))
Expand Down
1 change: 0 additions & 1 deletion pybamm/expression_tree/unary_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,6 @@ class Gradient_Squared(SpatialOperator):
operator with itself. In particular, this is useful in the finite element
formualtion where we only require the (sclar valued) square of the gradient,
and not the gradient itself.
**Extends:** :class:`SpatialOperator`
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ def _current_collector_heating(self, variables):
"""Returns the heat source terms in the 2D current collector"""
phi_s_cn = variables["Negative current collector potential"]
phi_s_cp = variables["Positive current collector potential"]
# Note: grad not implemented in 2D weak form, but can compute grad squared
# directly

Q_s_cn = self.param.sigma_cn_prime * pybamm.grad_squared(phi_s_cn)
Q_s_cp = self.param.sigma_cp_prime * pybamm.grad_squared(phi_s_cp)
return Q_s_cn, Q_s_cp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ def _current_collector_heating(self, variables):
"""Returns the heat source terms in the 2D current collector"""
phi_s_cn = variables["Negative current collector potential"]
phi_s_cp = variables["Positive current collector potential"]
# Note: grad not implemented in 2D weak form, but can compute grad squared
# directly

Q_s_cn = self.param.sigma_cn_prime * pybamm.grad_squared(phi_s_cn)
Q_s_cp = self.param.sigma_cp_prime * pybamm.grad_squared(phi_s_cp)
return Q_s_cn, Q_s_cp
Expand Down
112 changes: 99 additions & 13 deletions pybamm/spatial_methods/scikit_finite_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#
import pybamm

from scipy.sparse import csr_matrix
from scipy.sparse import csr_matrix, csc_matrix
from scipy.sparse.linalg import inv
import numpy as np
import skfem

Expand Down Expand Up @@ -67,10 +68,104 @@ def spatial_variable(self, symbol):
return vector

def gradient(self, symbol, discretised_symbol, boundary_conditions):
"""Matrix-vector multiplication to implement the gradient operator.
See :meth:`pybamm.SpatialMethod.gradient`
"""Matrix-vector multiplication to implement the gradient operator. The
gradient w of the function u is approximated by the finite element method
using the same function space as u, i.e. we solve w = grad(u), which
corresponds to the weak form w*v*dx = grad(u)*v*dx, where v is a suitable
test function.
Parameters
----------
symbol: :class:`pybamm.Symbol`
The symbol that we will take the laplacian of.
discretised_symbol: :class:`pybamm.Symbol`
The discretised symbol of the correct size
boundary_conditions : dict
The boundary conditions of the model
({symbol.id: {"negative tab": neg. tab bc, "positive tab": pos. tab bc}})
Returns
-------
:class: `pybamm.Concatenation`
A concatenation that contains the result of acting the discretised
gradient on the child discretised_symbol. The first column corresponds
to the y-component of the gradient and the second column corresponds
to the z component of the gradient.
"""
raise NotImplementedError
domain = symbol.domain[0]
mesh = self.mesh[domain][0]

# get gradient matrix
grad_y_matrix, grad_z_matrix = self.gradient_matrix(symbol, boundary_conditions)

# assemble mass matrix (there is no need to zero out entries here, since
# boundary conditions are already accounted for in the governing pde
# for the symbol we are taking the gradient of. we just want to get the
# correct weights)
@skfem.bilinear_form
def mass_form(u, du, v, dv, w):
return u * v

mass = skfem.asm(mass_form, mesh.basis)
# we need the inverse
mass_inv = pybamm.Matrix(inv(csc_matrix(mass)))

# compute gradient
grad_y = mass_inv @ (grad_y_matrix @ discretised_symbol)
grad_z = mass_inv @ (grad_z_matrix @ discretised_symbol)

# create concatenation
grad = pybamm.Concatenation(
grad_y, grad_z, check_domain=False, concat_fun=np.hstack
)
grad.domain = domain

return grad

def gradient_squared(self, symbol, discretised_symbol, boundary_conditions):
"""Multiplication to implement the inner product of the gradient operator
with itself. See :meth:`pybamm.SpatialMethod.gradient_squared`
"""
grad = self.gradient(symbol, discretised_symbol, boundary_conditions)
grad_y, grad_z = grad.orphans
return grad_y ** 2 + grad_z ** 2

def gradient_matrix(self, symbol, boundary_conditions):
"""
Gradient matrix for finite elements in the appropriate domain.
Parameters
----------
symbol: :class:`pybamm.Symbol`
The symbol for which we want to calculate the gradient matrix
boundary_conditions : dict
The boundary conditions of the model
({symbol.id: {"negative tab": neg. tab bc, "positive tab": pos. tab bc}})
Returns
-------
:class:`pybamm.Matrix`
The (sparse) finite element gradient matrix for the domain
"""
# get primary domain mesh
domain = symbol.domain[0]
mesh = self.mesh[domain][0]

# make form for the gradient in the y direction
@skfem.bilinear_form
def gradient_dy(u, du, v, dv, w):
return du[0] * v[0]

# make form for the gradient in the z direction
@skfem.bilinear_form
def gradient_dz(u, du, v, dv, w):
return du[1] * v[1]

# assemble the matrices
grad_y = skfem.asm(gradient_dy, mesh.basis)
grad_z = skfem.asm(gradient_dz, mesh.basis)

return pybamm.Matrix(grad_y), pybamm.Matrix(grad_z)

def divergence(self, symbol, discretised_symbol, boundary_conditions):
"""Matrix-vector multiplication to implement the divergence operator.
Expand Down Expand Up @@ -151,15 +246,6 @@ def unit_bc_load_form(v, dv, w):

return -stiffness_matrix @ discretised_symbol + boundary_load

def gradient_squared(self, symbol, discretised_symbol, boundary_conditions):
"""Matrix-vector multiplication to implement the inner product of the
gradient operator with itself.
See :meth:`pybamm.SpatialMethod.gradient_squared`
"""
stiffness_matrix = self.stiffness_matrix(symbol, boundary_conditions)

return stiffness_matrix @ (discretised_symbol ** 2)

def stiffness_matrix(self, symbol, boundary_conditions):
"""
Laplacian (stiffness) matrix for finite elements in the appropriate domain.
Expand Down
1 change: 0 additions & 1 deletion pybamm/spatial_methods/spatial_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ def gradient_squared(self, symbol, discretised_symbol, boundary_conditions):
The symbol that we will take the gradient of.
discretised_symbol: :class:`pybamm.Symbol`
The discretised symbol of the correct size
boundary_conditions : dict
The boundary conditions of the model
({symbol.id: {"left": left bc, "right": right bc}})
Expand Down
37 changes: 34 additions & 3 deletions tests/unit/test_spatial_methods/test_scikit_finite_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ def test_not_implemented(self):
spatial_method = pybamm.ScikitFiniteElement()
spatial_method.build(mesh)
self.assertEqual(spatial_method.mesh, mesh)
with self.assertRaises(NotImplementedError):
spatial_method.gradient(None, None, None)
with self.assertRaises(NotImplementedError):
spatial_method.divergence(None, None, None)
with self.assertRaises(NotImplementedError):
Expand Down Expand Up @@ -54,7 +52,6 @@ def test_discretise_equations(self):
pybamm.laplacian(var) - pybamm.source(unit_source, var, boundary=True),
pybamm.laplacian(var)
- pybamm.source(unit_source ** 2 + 1 / var, var, boundary=True),
pybamm.grad_squared(var),
]:
# Check that equation can be evaluated in each case
# Dirichlet
Expand Down Expand Up @@ -125,6 +122,40 @@ def test_discretise_equations(self):
with self.assertRaises(pybamm.GeometryError):
disc.process_symbol(x)

def test_gradient(self):
mesh = get_unit_2p1D_mesh_for_testing(ypts=32, zpts=32)
spatial_methods = {
"macroscale": pybamm.FiniteVolume(),
"current collector": pybamm.ScikitFiniteElement(),
}
disc = pybamm.Discretisation(mesh, spatial_methods)

# test gradient of 5*y + 6*z
var = pybamm.Variable("var", domain="current collector")
disc.set_variable_slices([var])

y = mesh["current collector"][0].coordinates[0, :]
z = mesh["current collector"][0].coordinates[1, :]

gradient = pybamm.grad(var)
grad_disc = disc.process_symbol(gradient)
grad_disc_y, grad_disc_z = grad_disc.children

np.testing.assert_array_almost_equal(
grad_disc_y.evaluate(None, 5 * y + 6 * z),
5 * np.ones_like(y)[:, np.newaxis],
)
np.testing.assert_array_almost_equal(
grad_disc_z.evaluate(None, 5 * y + 6 * z),
6 * np.ones_like(z)[:, np.newaxis],
)

# check grad_squared positive
eqn = pybamm.grad_squared(var)
eqn_disc = disc.process_symbol(eqn)
ans = eqn_disc.evaluate(None, 3 * y ** 2)
np.testing.assert_array_less(0, ans)

def test_manufactured_solution(self):
mesh = get_unit_2p1D_mesh_for_testing(ypts=32, zpts=32)
spatial_methods = {
Expand Down

0 comments on commit 3098ec6

Please sign in to comment.