diff --git a/docs/source/expression_tree/parameter.rst b/docs/source/expression_tree/parameter.rst
index bb6cdf6a..d49ca150 100644
--- a/docs/source/expression_tree/parameter.rst
+++ b/docs/source/expression_tree/parameter.rst
@@ -4,3 +4,5 @@ Parameter
 .. autoclass:: pybamm.Parameter
   :members:
 
+.. autoclass:: pybamm.FunctionParameter
+  :members:
diff --git a/pybamm/__init__.py b/pybamm/__init__.py
index 7dd7592b..6f87435a 100644
--- a/pybamm/__init__.py
+++ b/pybamm/__init__.py
@@ -67,7 +67,6 @@ from .expression_tree.concatenations import (
 )
 from .expression_tree.array import Array
 from .expression_tree.matrix import Matrix
-from .expression_tree.parameter import Parameter
 from .expression_tree.unary_operators import (
     UnaryOperator,
     Negate,
@@ -82,8 +81,8 @@ from .expression_tree.unary_operators import (
     div,
     surf,
 )
+from .expression_tree.parameter import Parameter, FunctionParameter
 from .expression_tree.broadcasts import Broadcast, NumpyBroadcast
-from .expression_tree.function_parameter import FunctionParameter
 from .expression_tree.scalar import Scalar
 from .expression_tree.variable import Variable
 from .expression_tree.independent_variable import (
diff --git a/pybamm/expression_tree/function_parameter.py b/pybamm/expression_tree/function_parameter.py
deleted file mode 100644
index 7cc3a1cd..00000000
--- a/pybamm/expression_tree/function_parameter.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#
-# Parameter class
-#
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-import pybamm
-
-
-class FunctionParameter(pybamm.UnaryOperator):
-    """A node in the expression tree representing a function parameter
-
-    This node will be replaced by a :class:`.Function` node
-
-    Parameters
-    ----------
-
-    name : str
-        name of the node
-    child : :class:`Symbol`
-        child node
-    domain : iterable of str, optional
-        list of domains the parameter is valid over, defaults to empty list
-
-    """
-
-    def __init__(self, name, child):
-        super().__init__(name, child)
diff --git a/pybamm/expression_tree/parameter.py b/pybamm/expression_tree/parameter.py
index 1195356c..f305972c 100644
--- a/pybamm/expression_tree/parameter.py
+++ b/pybamm/expression_tree/parameter.py
@@ -1,5 +1,5 @@
 #
-# Parameter class
+# Parameter classes
 #
 from __future__ import absolute_import, division
 from __future__ import print_function, unicode_literals
@@ -23,3 +23,34 @@ class Parameter(pybamm.Symbol):
 
     def __init__(self, name, domain=[]):
         super().__init__(name, domain=domain)
+
+
+class FunctionParameter(pybamm.UnaryOperator):
+    """A node in the expression tree representing a function parameter
+
+    This node will be replaced by a :class:`pybamm.Function` node
+
+    Parameters
+    ----------
+
+    name : str
+        name of the node
+    child : :class:`Symbol`
+        child node
+    diff_variable : :class:`pybamm.Symbol`, optional
+        if diff_variable is specified, the FunctionParameter node will be replaced by a
+        :class:`pybamm.Function` and then differentiated with respect to diff_variable.
+        Default is None.
+
+    """
+
+    def __init__(self, name, child, diff_variable=None):
+        super().__init__(name, child)
+        # assign diff variable
+        self.diff_variable = diff_variable
+
+    def diff(self, variable):
+        """ See :meth:`pybamm.Symbol.diff()`. """
+        # return a new FunctionParameter, that knows it will need to be differentiated
+        # when the parameters are set
+        return FunctionParameter(self.name, self.orphans[0], diff_variable=variable)
diff --git a/pybamm/expression_tree/scalar.py b/pybamm/expression_tree/scalar.py
index 70cfa0a8..c03ea53f 100644
--- a/pybamm/expression_tree/scalar.py
+++ b/pybamm/expression_tree/scalar.py
@@ -54,7 +54,7 @@ class Scalar(pybamm.Symbol):
 
     @value.setter
     def value(self, value):
-        self._value = value
+        self._value = float(value)
 
     def evaluate(self, t=None, y=None):
         """ See :meth:`pybamm.Symbol.evaluate()`. """
diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py
index e7e48560..cfe589b6 100644
--- a/pybamm/parameters/parameter_values.py
+++ b/pybamm/parameters/parameter_values.py
@@ -152,7 +152,13 @@ class ParameterValues(dict):
         elif isinstance(symbol, pybamm.FunctionParameter):
             new_child = self.process_symbol(symbol.children[0])
             function_name = self.get_parameter_value(symbol)
-            return pybamm.Function(pybamm.load_function(function_name), new_child)
+            function = pybamm.Function(pybamm.load_function(function_name), new_child)
+            if symbol.diff_variable is None:
+                return function
+            else:
+                # return differentiated function
+                new_diff_variable = self.process_symbol(symbol.children[0])
+                return function.diff(new_diff_variable)
 
         elif isinstance(symbol, pybamm.BinaryOperator):
             left, right = symbol.children

Symbolic differentiation
We need a function (e.g. `pybamm.diff`) that calculates the derivative of an expression tree with respect to a variable. For example, some lead-acid models require the user to provide a function for the open-circuit potential `U(c)` while others require its derivative `dU/dc` (and also for the exchange-current density). 

We could either ask the user to provide the appropriate derivatives manually at the same time as the functions themselves, or calculate the derivative using the expression tree and appropriate applications of the chain rule and power rule. I think the latter is safer
diff --git a/tests/test_discretisations/test_discretisation.py b/tests/test_discretisations/test_discretisation.py
index dfd82b0d..f2c4200f 100644
--- a/tests/test_discretisations/test_discretisation.py
+++ b/tests/test_discretisations/test_discretisation.py
@@ -149,10 +149,10 @@ class TestDiscretise(unittest.TestCase):
     def test_process_complex_expression(self):
         var1 = pybamm.Variable("var1")
         var2 = pybamm.Variable("var2")
-        scal1 = pybamm.Scalar("scal1")
-        scal2 = pybamm.Scalar("scal2")
-        scal3 = pybamm.Scalar("scal3")
-        scal4 = pybamm.Scalar("scal4")
+        scal1 = pybamm.Scalar(1)
+        scal2 = pybamm.Scalar(2)
+        scal3 = pybamm.Scalar(3)
+        scal4 = pybamm.Scalar(4)
         expression = (scal1 * (scal3 + var2)) / ((var1 - scal4) + scal2)
 
         # create discretisation
diff --git a/tests/test_expression_tree/test_parameter.py b/tests/test_expression_tree/test_parameter.py
index aa51851a..5db9dd7e 100644
--- a/tests/test_expression_tree/test_parameter.py
+++ b/tests/test_expression_tree/test_parameter.py
@@ -17,6 +17,21 @@ class TestParameter(unittest.TestCase):
         self.assertEqual(c.domain[0], "test")
 
 
+class TestFunctionParameter(unittest.TestCase):
+    def test_function_parameter_init(self):
+        var = pybamm.Variable("var")
+        func = pybamm.FunctionParameter("func", var)
+        self.assertEqual(func.name, "func")
+        self.assertEqual(func.children[0].id, var.id)
+        self.assertEqual(func.domain, [])
+        self.assertEqual(func.diff_variable, None)
+
+    def test_function_parameter_diff(self):
+        var = pybamm.Variable("var")
+        func = pybamm.FunctionParameter("a", var).diff(var)
+        self.assertEqual(func.diff_variable, var)
+
+
 if __name__ == "__main__":
     print("Add -v for more debug output")
     import sys
diff --git a/tests/test_parameters/test_parameter_values.py b/tests/test_parameters/test_parameter_values.py
index 3bc421b2..62328b10 100644
--- a/tests/test_parameters/test_parameter_values.py
+++ b/tests/test_parameters/test_parameter_values.py
@@ -145,13 +145,18 @@ class TestParameterValues(unittest.TestCase):
         self.assertIsInstance(processed_func, pybamm.Function)
         self.assertEqual(processed_func.evaluate(), 369)
 
+        # process differentiated function parameter
+        diff_func = func.diff(a)
+        processed_diff_func = parameter_values.process_symbol(diff_func)
+        self.assertEqual(processed_diff_func.evaluate(), 123)
+
     def test_process_complex_expression(self):
         var1 = pybamm.Variable("var1")
         var2 = pybamm.Variable("var2")
         par1 = pybamm.Parameter("par1")
         par2 = pybamm.Parameter("par2")
-        scal1 = pybamm.Scalar("scal1")
-        scal2 = pybamm.Scalar("scal2")
+        scal1 = pybamm.Scalar(3)
+        scal2 = pybamm.Scalar(4)
         expression = (scal1 * (par1 + var2)) / ((var1 - par2) + scal2)
 
         param = pybamm.ParameterValues(base_parameters={"par1": 1, "par2": 2})

pip install -e .[dev,docs]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc gfortran libopenblas-dev liblapack-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
anyio==3.6.2
anytree==2.8.0
argon2-cffi==21.3.0
argon2-cffi-bindings==21.2.0
async-generator==1.10
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
autograd==1.6.2
Babel==2.11.0
backcall==0.2.0
bleach==4.1.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
comm==0.1.4
contextvars==2.4
cycler==0.11.0
dataclasses==0.8
decorator==5.1.1
defusedxml==0.7.1
docutils==0.17.1
entrypoints==0.4
flake8==5.0.4
future==1.0.0
guzzle-sphinx-theme==0.7.11
idna==3.10
imagesize==1.4.1
immutables==0.19
importlib-metadata==4.2.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
ipykernel==5.5.6
ipython==7.16.3
ipython-genutils==0.2.0
ipywidgets==7.8.5
jedi==0.17.2
Jinja2==3.0.3
json5==0.9.16
jsonschema==3.2.0
jupyter==1.1.1
jupyter-client==7.1.2
jupyter-console==6.4.3
jupyter-core==4.9.2
jupyter-server==1.13.1
jupyterlab==3.2.9
jupyterlab-pygments==0.1.2
jupyterlab-server==2.10.3
jupyterlab_widgets==1.1.11
kiwisolver==1.3.1
MarkupSafe==2.0.1
matplotlib==3.3.4
mccabe==0.7.0
mistune==0.8.4
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
nbclassic==0.3.5
nbclient==0.5.9
nbconvert==6.0.7
nbformat==5.1.3
nest-asyncio==1.6.0
notebook==6.4.10
numpy==1.19.5
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pandas==1.1.5
pandocfilters==1.5.1
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
Pillow==8.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
prometheus-client==0.17.1
prompt-toolkit==3.0.36
ptyprocess==0.7.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
-e git+https://github.com/tinosulzer/PyBaMM.git@96a9b423b46d84ae8d96aec5a705e833b1e5c4ae#egg=pybamm
pycodestyle==2.9.1
pycparser==2.21
pyflakes==2.5.0
Pygments==2.14.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pyrsistent==0.18.0
pytest==6.2.4
python-dateutil==2.9.0.post0
pytz==2025.2
pyzmq==25.1.2
requests==2.27.1
scipy==1.5.4
Send2Trash==1.8.3
six==1.17.0
sniffio==1.2.0
snowballstemmer==2.2.0
Sphinx==4.3.2
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
terminado==0.12.1
testpath==0.6.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tornado==6.1
traitlets==4.3.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
wcwidth==0.2.13
webencodings==0.5.1
websocket-client==1.3.1
widgetsnbextension==3.6.10
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: PyBaMM
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - anyio==3.6.2
      - anytree==2.8.0
      - argon2-cffi==21.3.0
      - argon2-cffi-bindings==21.2.0
      - async-generator==1.10
      - autograd==1.6.2
      - babel==2.11.0
      - backcall==0.2.0
      - bleach==4.1.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - comm==0.1.4
      - contextvars==2.4
      - cycler==0.11.0
      - dataclasses==0.8
      - decorator==5.1.1
      - defusedxml==0.7.1
      - docutils==0.17.1
      - entrypoints==0.4
      - flake8==5.0.4
      - future==1.0.0
      - guzzle-sphinx-theme==0.7.11
      - idna==3.10
      - imagesize==1.4.1
      - immutables==0.19
      - importlib-metadata==4.2.0
      - ipykernel==5.5.6
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - ipywidgets==7.8.5
      - jedi==0.17.2
      - jinja2==3.0.3
      - json5==0.9.16
      - jsonschema==3.2.0
      - jupyter==1.1.1
      - jupyter-client==7.1.2
      - jupyter-console==6.4.3
      - jupyter-core==4.9.2
      - jupyter-server==1.13.1
      - jupyterlab==3.2.9
      - jupyterlab-pygments==0.1.2
      - jupyterlab-server==2.10.3
      - jupyterlab-widgets==1.1.11
      - kiwisolver==1.3.1
      - markupsafe==2.0.1
      - matplotlib==3.3.4
      - mccabe==0.7.0
      - mistune==0.8.4
      - nbclassic==0.3.5
      - nbclient==0.5.9
      - nbconvert==6.0.7
      - nbformat==5.1.3
      - nest-asyncio==1.6.0
      - notebook==6.4.10
      - numpy==1.19.5
      - pandas==1.1.5
      - pandocfilters==1.5.1
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pillow==8.4.0
      - prometheus-client==0.17.1
      - prompt-toolkit==3.0.36
      - ptyprocess==0.7.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pyflakes==2.5.0
      - pygments==2.14.0
      - pyrsistent==0.18.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyzmq==25.1.2
      - requests==2.27.1
      - scipy==1.5.4
      - send2trash==1.8.3
      - six==1.17.0
      - sniffio==1.2.0
      - snowballstemmer==2.2.0
      - sphinx==4.3.2
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - terminado==0.12.1
      - testpath==0.6.0
      - tornado==6.1
      - traitlets==4.3.3
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - webencodings==0.5.1
      - websocket-client==1.3.1
      - widgetsnbextension==3.6.10
prefix: /opt/conda/envs/PyBaMM

tests/test_expression_tree/test_parameter.py::TestFunctionParameter::test_function_parameter_diff tests/test_expression_tree/test_parameter.py::TestFunctionParameter::test_function_parameter_init tests/test_parameters/test_parameter_values.py::TestParameterValues::test_process_function_parameter
tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_model_concatenation tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_model_dae tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_model_ode tests/test_parameters/test_parameter_values.py::TestParameterValues::test_process_model
tests/test_discretisations/test_discretisation.py::TestDiscretise::test_broadcast tests/test_discretisations/test_discretisation.py::TestDiscretise::test_concatenate_init tests/test_discretisations/test_discretisation.py::TestDiscretise::test_concatenation tests/test_discretisations/test_discretisation.py::TestDiscretise::test_concatenation_of_scalars tests/test_discretisations/test_discretisation.py::TestDiscretise::test_core_NotImplementedErrors tests/test_discretisations/test_discretisation.py::TestDiscretise::test_discretise_slicing tests/test_discretisations/test_discretisation.py::TestDiscretise::test_discretise_spatial_operator tests/test_discretisations/test_discretisation.py::TestDiscretise::test_discretise_spatial_variable tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_complex_expression tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_dict tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_symbol_base tests/test_discretisations/test_discretisation.py::TestDiscretise::test_process_variables_dict tests/test_expression_tree/test_parameter.py::TestParameter::test_parameter_init tests/test_parameters/test_parameter_values.py::TestParameterValues::test_get_parameter_value tests/test_parameters/test_parameter_values.py::TestParameterValues::test_init tests/test_parameters/test_parameter_values.py::TestParameterValues::test_overwrite tests/test_parameters/test_parameter_values.py::TestParameterValues::test_process_complex_expression tests/test_parameters/test_parameter_values.py::TestParameterValues::test_process_symbol tests/test_parameters/test_parameter_values.py::TestParameterValues::test_read_parameters_csv
pybamm/__init__.py pybamm/parameters/parameter_values.py pybamm/expression_tree/function_parameter.py pybamm/expression_tree/scalar.py pybamm/expression_tree/parameter.py docs/source/expression_tree/parameter.rst
pybamm/__init__.py pybamm/parameters/parameter_values.py pybamm/expression_tree/function_parameter.py pybamm/expression_tree/scalar.py pybamm/expression_tree/parameter.py docs/source/expression_tree/parameter.rst