Repository: tinosulzer/PyBaMM

Issue:
Get surface values
**Summary**
Implement a class which takes a variable and returns the value at the surface. 

**Motivation**
This is required for implementing the SPM #72 as the concentrations on the surface of the particles are needed in the Butler-Volmer expression and the OCPs.

Discussion:

Patch:
diff --git a/docs/source/expression_tree/unary_operator.rst b/docs/source/expression_tree/unary_operator.rst
index 79ece869..13675c49 100644
--- a/docs/source/expression_tree/unary_operator.rst
+++ b/docs/source/expression_tree/unary_operator.rst
@@ -25,6 +25,11 @@ Unary Operators
 .. autoclass:: pybamm.NumpyBroadcast
   :members:
 
+.. autoclass:: pybamm.SurfaceValue
+  :members:
+
 .. autofunction:: pybamm.grad
 
 .. autofunction:: pybamm.div
+
+.. autofunction:: pybamm.surf
diff --git a/pybamm/__init__.py b/pybamm/__init__.py
index ac4b6da7..98c5b202 100644
--- a/pybamm/__init__.py
+++ b/pybamm/__init__.py
@@ -78,8 +78,10 @@ from .expression_tree.unary_operators import (
     Divergence,
     Broadcast,
     NumpyBroadcast,
+    SurfaceValue,
     grad,
     div,
+    surf,
 )
 from .expression_tree.function_parameter import FunctionParameter
 from .expression_tree.scalar import Scalar
diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py
index a572b4fb..18cff23e 100644
--- a/pybamm/discretisations/discretisation.py
+++ b/pybamm/discretisations/discretisation.py
@@ -212,6 +212,13 @@ class Discretisation(object):
                 )
             return symbol
 
+        elif isinstance(symbol, pybamm.SurfaceValue):
+            child = symbol.children[0]
+            discretised_child = self.process_symbol(child)
+            return self._spatial_methods[symbol.domain[0]].surface_value(
+                discretised_child
+            )
+
         elif isinstance(symbol, pybamm.BinaryOperator):
             return self.process_binary_operators(symbol)
 
diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py
index 17ce41fd..abe2babb 100644
--- a/pybamm/expression_tree/unary_operators.py
+++ b/pybamm/expression_tree/unary_operators.py
@@ -210,6 +210,16 @@ class NumpyBroadcast(Broadcast):
             return child_eval * self.broadcasting_vector
 
 
+class SurfaceValue(SpatialOperator):
+    """A node in the expression tree which gets the surface value of a variable.
+
+    **Extends:** :class:`SpatialOperator`
+    """
+
+    def __init__(self, child):
+        super().__init__("surf", child)
+
+
 #
 # Methods to call Gradient and Divergence
 #
@@ -251,3 +261,27 @@ def div(expression):
     """
 
     return Divergence(expression)
+
+
+#
+# Method to call SurfaceValue
+#
+
+
+def surf(variable):
+    """convenience function for creating a :class:`SurfaceValue`
+
+    Parameters
+    ----------
+
+    variable : :class:`Symbol`
+        the surface value of this variable will be returned
+
+    Returns
+    -------
+
+    :class:`GetSurfaceValue`
+        the surface value of ``variable``
+    """
+
+    return SurfaceValue(variable)
diff --git a/pybamm/spatial_methods/finite_volume.py b/pybamm/spatial_methods/finite_volume.py
index b172ad3d..86cc3f68 100644
--- a/pybamm/spatial_methods/finite_volume.py
+++ b/pybamm/spatial_methods/finite_volume.py
@@ -237,6 +237,33 @@ class FiniteVolume(pybamm.SpatialMethod):
             left_ghost_cell, discretised_symbol, right_ghost_cell
         )
 
+    def surface_value(self, discretised_symbol):
+        """
+        Uses linear extrapolation to get the surface value of a variable in the
+        Finite Volume Method.
+
+        Parameters
+        -----------
+        discretised_symbol : :class:`pybamm.StateVector`
+            The discretised variable (a state vector) from which to calculate
+            the surface value.
+
+        Returns
+        -------
+        :class:`pybamm.Variable`
+            The variable representing the surface value.
+        """
+        # Better to make class similar NodeToEdge and pass function?
+        # def surface_value(array):
+        #     "Linear extrapolation for surface value"
+        #     array[-1] + (array[-1] - array[-2]) / 2
+        # ... or make StateVector and add?
+        y_slice_stop = discretised_symbol.y_slice.stop
+        last_node = pybamm.StateVector(slice(y_slice_stop - 1, y_slice_stop))
+        penultimate_node = pybamm.StateVector(slice(y_slice_stop - 2, y_slice_stop - 1))
+        surface_value = (last_node + (last_node - penultimate_node) / 2)
+        return surface_value
+
     #######################################################
     # Can probably be moved outside of the spatial method
     ######################################################
diff --git a/pybamm/spatial_methods/spatial_method.py b/pybamm/spatial_methods/spatial_method.py
index 1006ed32..b92104b7 100644
--- a/pybamm/spatial_methods/spatial_method.py
+++ b/pybamm/spatial_methods/spatial_method.py
@@ -102,6 +102,24 @@ class SpatialMethod:
         """
         raise NotImplementedError
 
+    def surface_value(self, discretised_symbol):
+        """
+        Returns the surface value using the approriate expression for the
+        spatial method.
+
+        Parameters
+        -----------
+        discretised_symbol : :class:`pybamm.StateVector`
+            The discretised variable (a state vector) from which to calculate
+            the surface value.
+
+        Returns
+        -------
+        :class:`pybamm.Variable`
+            The variable representing the surface value.
+        """
+        raise NotImplementedError
+
     # We could possibly move the following outside of SpatialMethod
     # depending on the requirements of the FiniteVolume
 


Test patch:
diff --git a/tests/test_spatial_methods/test_finite_volume.py b/tests/test_spatial_methods/test_finite_volume.py
index 7c7a2e71..ce84a225 100644
--- a/tests/test_spatial_methods/test_finite_volume.py
+++ b/tests/test_spatial_methods/test_finite_volume.py
@@ -32,6 +32,31 @@ class TestFiniteVolume(unittest.TestCase):
         avd = pybamm.NodeToEdge(d, arithmetic_mean)
         np.testing.assert_array_equal(avd.evaluate(None, y_test), np.ones(9))
 
+    def test_surface_value(self):
+        # create discretisation
+        defaults = shared.TestDefaults1DParticle(10)
+        spatial_methods = {"negative particle": pybamm.FiniteVolume}
+        disc = pybamm.Discretisation(defaults.mesh, spatial_methods)
+        mesh = disc.mesh
+
+        combined_submesh = mesh.combine_submeshes("negative particle")
+
+        # create variable
+        var = pybamm.Variable("var", domain="negative particle")
+        surf_eqn = pybamm.surf(var)
+        disc._variables = [var]
+        disc.set_variable_slices()
+        surf_eqn_disc = disc.process_symbol(surf_eqn)
+
+        # check constant extrapolates to constant
+        constant_y = np.ones_like(combined_submesh.nodes)
+        self.assertEqual(surf_eqn_disc.evaluate(None, constant_y), 1)
+
+        # check linear variable extrapolates correctly
+        linear_y = combined_submesh.nodes
+        y_surf = combined_submesh.nodes[-1] + combined_submesh.d_nodes[-1] / 2
+        self.assertEqual(surf_eqn_disc.evaluate(None, linear_y), y_surf)
+
     def test_discretise_diffusivity_times_spatial_operator(self):
         # Set up
         whole_cell = ["negative electrode", "separator", "positive electrode"]