Repository: pybamm-team/PyBaMM

Issue:
Create a pybamm.ParameterValues from a BPX file
### Description

https://github.com/pybamm-team/BPX defines a battery model parameter exchange format. This issue proposes a function in pybamm that will take a BPX file and return a `pybamm.ParameterValues` object that can be used to simulate from a DFN model

### Motivation

To allow a BPX=>pybamm workflow

### Possible Implementation

_No response_

### Additional context

_No response_

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 076d47c64..fc701aa0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Features
 
+- Added functionality to create `pybamm.ParameterValues` from a [BPX standard](https://github.com/pybamm-team/BPX) JSON file ([#2555](https://github.com/pybamm-team/PyBaMM/pull/2555)).
 - Allow the option "surface form" to be "differential" in the `MPM` ([#2533](https://github.com/pybamm-team/PyBaMM/pull/2533))
 - Added variables "Loss of lithium due to loss of active material in negative/positive electrode [mol]". These should be included in the calculation of "total lithium in system" to make sure that lithium is truly conserved. ([#2529](https://github.com/pybamm-team/PyBaMM/pull/2529))
 - `initial_soc` can now be a string "x V", in which case the simulation is initialized to start from that voltage ([#2508](https://github.com/pybamm-team/PyBaMM/pull/2508))
diff --git a/pybamm/parameters/bpx.py b/pybamm/parameters/bpx.py
new file mode 100644
index 000000000..61f244516
--- /dev/null
+++ b/pybamm/parameters/bpx.py
@@ -0,0 +1,381 @@
+from bpx import BPX, Function, InterpolatedTable
+import pybamm
+import math
+from dataclasses import dataclass
+import numpy as np
+from pybamm import constants
+from pybamm import exp
+import copy
+
+
+import types
+import functools
+
+
+def _copy_func(f):
+    """Based on http://stackoverflow.com/a/6528148/190597 (Glenn Maynard)"""
+    g = types.FunctionType(
+        f.__code__,
+        f.__globals__,
+        name=f.__name__,
+        argdefs=f.__defaults__,
+        closure=f.__closure__,
+    )
+    g = functools.update_wrapper(g, f)
+    g.__kwdefaults__ = f.__kwdefaults__
+    return g
+
+
+@dataclass
+class Domain:
+    name: str
+    pre_name: str
+    short_pre_name: str
+
+
+cell = Domain(name="cell", pre_name="", short_pre_name="")
+negative_electrode = Domain(
+    name="negative electrode",
+    pre_name="Negative electrode ",
+    short_pre_name="Negative ",
+)
+positive_electrode = Domain(
+    name="positive electrode",
+    pre_name="Positive electrode ",
+    short_pre_name="Positive ",
+)
+electrolyte = Domain(name="electrolyte", pre_name="Electrolyte ", short_pre_name="")
+separator = Domain(name="separator", pre_name="Separator ", short_pre_name="")
+experiment = Domain(name="experiment", pre_name="", short_pre_name="")
+
+
+def _bpx_to_param_dict(bpx: BPX) -> dict:
+    pybamm_dict = {}
+    pybamm_dict = _bpx_to_domain_param_dict(
+        bpx.parameterisation.cell, pybamm_dict, cell
+    )
+    pybamm_dict = _bpx_to_domain_param_dict(
+        bpx.parameterisation.negative_electrode, pybamm_dict, negative_electrode
+    )
+    pybamm_dict = _bpx_to_domain_param_dict(
+        bpx.parameterisation.positive_electrode, pybamm_dict, positive_electrode
+    )
+    pybamm_dict = _bpx_to_domain_param_dict(
+        bpx.parameterisation.electrolyte, pybamm_dict, electrolyte
+    )
+    pybamm_dict = _bpx_to_domain_param_dict(
+        bpx.parameterisation.separator, pybamm_dict, separator
+    )
+    pybamm_dict = _bpx_to_domain_param_dict(
+        bpx.parameterisation.separator, pybamm_dict, experiment
+    )
+
+    # set a default current function and typical current based on the nominal capacity
+    # i.e. a default C-rate of 1
+    pybamm_dict["Current function [A]"] = pybamm_dict["Nominal cell capacity [A.h]"]
+    pybamm_dict["Typical current [A]"] = pybamm_dict["Nominal cell capacity [A.h]"]
+
+    # number of electrons in reaction (1 for li-ion)
+    for domain in [negative_electrode, positive_electrode]:
+        pybamm_dict[domain.pre_name + "electrons in reaction"] = 1.0
+
+    # activity
+    pybamm_dict["1 + dlnf/dlnc"] = 1.0
+
+    # typical electrolyte concentration
+    pybamm_dict["Typical electrolyte concentration [mol.m-3]"] = pybamm_dict[
+        "Initial concentration in electrolyte [mol.m-3]"
+    ]
+
+    # assume Bruggeman relation
+    for domain in [negative_electrode, separator, positive_electrode]:
+        pybamm_dict[domain.pre_name + "Bruggeman coefficient (electrolyte)"] = 1.5
+        pybamm_dict[domain.pre_name + "Bruggeman coefficient (electrode)"] = 1.5
+
+    # BPX is for single cell in series, user can change this later
+    pybamm_dict["Number of cells connected in series to make a battery"] = 1
+    pybamm_dict[
+        "Number of electrodes connected in parallel to make a cell"
+    ] = pybamm_dict["Number of electrode pairs connected in parallel to make a cell"]
+
+    # electrode area
+    equal_len_width = math.sqrt(pybamm_dict["Electrode area [m2]"])
+    pybamm_dict["Electrode width [m]"] = equal_len_width
+    pybamm_dict["Electrode height [m]"] = equal_len_width
+
+    # surface area
+    pybamm_dict["Cell cooling surface area [m2]"] = pybamm_dict[
+        "External surface area [m2]"
+    ]
+
+    # volume
+    pybamm_dict["Cell volume [m3]"] = pybamm_dict["Volume [m3]"]
+
+    # reference temperature
+    T_ref = pybamm_dict["Reference temperature [K]"]
+
+    # lumped parameters
+    for name in [
+        "Specific heat capacity [J.K-1.kg-1]",
+        "Density [kg.m-3]",
+        "Thermal conductivity [W.m-1.K-1]",
+    ]:
+        for domain in [negative_electrode, positive_electrode, separator]:
+            pybamm_name = domain.pre_name + name[:1].lower() + name[1:]
+            if name in pybamm_dict:
+                pybamm_dict[pybamm_name] = pybamm_dict[name]
+
+    # BET surface area
+    for domain in [negative_electrode, positive_electrode]:
+        pybamm_dict[domain.pre_name + "active material volume fraction"] = (
+            pybamm_dict[domain.pre_name + "surface area per unit volume [m-1]"]
+            * pybamm_dict[domain.short_pre_name + "particle radius [m]"]
+        ) / 3.0
+
+    # transport efficiency
+    for domain in [negative_electrode, separator, positive_electrode]:
+        pybamm_dict[domain.pre_name + "porosity"] = pybamm_dict[
+            domain.pre_name + "transport efficiency"
+        ] ** (1.0 / 1.5)
+
+    # TODO: allow setting function parameters in a loop over domains
+
+    # entropic change
+
+    # negative electrode
+    U_n = pybamm_dict[
+        negative_electrode.pre_name + "entropic change coefficient [V.K-1]"
+    ]
+    if callable(U_n):
+
+        def _negative_electrode_entropic_change(sto, c_s_max):
+            return U_n(sto)
+
+    else:
+
+        def _negative_electrode_entropic_change(sto, c_s_max):
+            return U_n
+
+    pybamm_dict[
+        negative_electrode.pre_name + "OCP entropic change [V.K-1]"
+    ] = _negative_electrode_entropic_change
+
+    # positive electrode
+    U_p = pybamm_dict[
+        positive_electrode.pre_name + "entropic change coefficient [V.K-1]"
+    ]
+    if callable(U_p):
+
+        def _positive_electrode_entropic_change(sto, c_s_max):
+            return U_p(sto)
+
+    else:
+
+        def _positive_electrode_entropic_change(sto, c_s_max):
+            return U_p
+
+    pybamm_dict[
+        positive_electrode.pre_name + "OCP entropic change [V.K-1]"
+    ] = _positive_electrode_entropic_change
+
+    # reaction rates in pybamm exchange current is defined j0 = k * sqrt(ce * cs *
+    # (cs-cs_max)) in BPX exchange current is defined j0 = F * k_norm * sqrt((ce/ce0) *
+    # (cs/cs_max) * (1-cs/cs_max))
+    c_e = pybamm_dict["Typical electrolyte concentration [mol.m-3]"]
+    F = 96485
+
+    # negative electrode
+    c_n_max = pybamm_dict[
+        "Maximum concentration in " + negative_electrode.pre_name.lower() + "[mol.m-3]"
+    ]
+    k_n_norm = pybamm_dict[
+        negative_electrode.pre_name + "reaction rate constant [mol.m-2.s-1]"
+    ]
+    E_a_n = pybamm_dict.get(
+        negative_electrode.pre_name + "reaction rate activation energy [J.mol-1]", 0.0
+    )
+    k_n = k_n_norm * F / (c_n_max * c_e**0.5)
+
+    def _negative_electrode_exchange_current_density(c_e, c_s_surf, c_s_max, T):
+        k_ref = k_n  # (A/m2)(m3/mol)**1.5 - includes ref concentrations
+
+        arrhenius = exp(E_a_n / constants.R * (1 / T_ref - 1 / T))
+        return (
+            k_ref
+            * arrhenius
+            * c_e**0.5
+            * c_s_surf**0.5
+            * (c_s_max - c_s_surf) ** 0.5
+        )
+
+    pybamm_dict[
+        negative_electrode.pre_name + "exchange-current density [A.m-2]"
+    ] = _copy_func(_negative_electrode_exchange_current_density)
+
+    # positive electrode
+    c_p_max = pybamm_dict[
+        "Maximum concentration in " + positive_electrode.pre_name.lower() + "[mol.m-3]"
+    ]
+    k_p_norm = pybamm_dict[
+        positive_electrode.pre_name + "reaction rate constant [mol.m-2.s-1]"
+    ]
+    E_a_p = pybamm_dict.get(
+        positive_electrode.pre_name + "reaction rate activation energy [J.mol-1]", 0.0
+    )
+    k_p = k_p_norm * F / (c_p_max * c_e**0.5)
+
+    def _positive_electrode_exchange_current_density(c_e, c_s_surf, c_s_max, T):
+        k_ref = k_p  # (A/m2)(m3/mol)**1.5 - includes ref concentrations
+
+        arrhenius = exp(E_a_p / constants.R * (1 / T_ref - 1 / T))
+        return (
+            k_ref
+            * arrhenius
+            * c_e**0.5
+            * c_s_surf**0.5
+            * (c_s_max - c_s_surf) ** 0.5
+        )
+
+    pybamm_dict[domain.pre_name + "exchange-current density [A.m-2]"] = _copy_func(
+        _positive_electrode_exchange_current_density
+    )
+
+    # diffusivity
+
+    # negative electrode
+    E_a = pybamm_dict.get(
+        negative_electrode.pre_name + "diffusivity activation energy [J.mol-1]", 0.0
+    )
+    D_n_ref = pybamm_dict[negative_electrode.pre_name + "diffusivity [m2.s-1]"]
+
+    if callable(D_n_ref):
+
+        def _negative_electrode_diffusivity(sto, T):
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * D_n_ref(sto)
+
+    else:
+
+        def _negative_electrode_diffusivity(sto, T):
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * D_n_ref
+
+    pybamm_dict[negative_electrode.pre_name + "diffusivity [m2.s-1]"] = _copy_func(
+        _negative_electrode_diffusivity
+    )
+
+    # positive electrode
+    E_a = pybamm_dict.get(
+        positive_electrode.pre_name + "diffusivity activation energy [J.mol-1]", 0.0
+    )
+    D_p_ref = pybamm_dict[positive_electrode.pre_name + "diffusivity [m2.s-1]"]
+
+    if callable(D_p_ref):
+
+        def _positive_electrode_diffusivity(sto, T):
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * D_p_ref(sto)
+
+    else:
+
+        def _positive_electrode_diffusivity(sto, T):
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * D_p_ref
+
+    pybamm_dict[positive_electrode.pre_name + "diffusivity [m2.s-1]"] = _copy_func(
+        _positive_electrode_diffusivity
+    )
+
+    # electrolyte
+    E_a = pybamm_dict.get(
+        electrolyte.pre_name + "diffusivity activation energy [J.mol-1]", 0.0
+    )
+    D_e_ref = pybamm_dict[electrolyte.pre_name + "diffusivity [m2.s-1]"]
+
+    if callable(D_e_ref):
+
+        def _electrolyte_diffusivity(sto, T):
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * D_e_ref(sto)
+
+    else:
+
+        def _electrolyte_diffusivity(sto, T):
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * D_e_ref
+
+    pybamm_dict[electrolyte.pre_name + "diffusivity [m2.s-1]"] = _copy_func(
+        _electrolyte_diffusivity
+    )
+
+    # conductivity
+    E_a = pybamm_dict.get(
+        electrolyte.pre_name + "conductivity activation energy [J.mol-1]", 0.0
+    )
+    C_ref_value = pybamm_dict[electrolyte.pre_name + "conductivity [S.m-1]"]
+
+    if callable(C_ref_value):
+        C_ref_fun = copy.copy(C_ref_value)
+
+        def _conductivity(c_e, T):
+            C_ref = C_ref_fun(c_e)
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * C_ref
+
+    else:
+        C_ref_number = C_ref_value
+
+        def _conductivity(c_e, T):
+            C_ref = C_ref_number
+            arrhenius = exp(E_a / constants.R * (1 / T_ref - 1 / T))
+            return arrhenius * C_ref
+
+    pybamm_dict[electrolyte.pre_name + "conductivity [S.m-1]"] = _copy_func(
+        _conductivity
+    )
+
+    return pybamm_dict
+
+
+preamble = "from pybamm import exp, tanh, cosh\n\n"
+
+
+def _bpx_to_domain_param_dict(instance: BPX, pybamm_dict: dict, domain: Domain) -> dict:
+    for name, field in instance.__fields__.items():
+        value = getattr(instance, name)
+        if value is None:
+            continue
+        elif isinstance(value, Function):
+            value = value.to_python_function(preamble=preamble)
+        elif isinstance(value, InterpolatedTable):
+            timescale = 1
+            x = np.array(value.x)
+            y = np.array(value.y)
+            interpolator = "linear"
+            value = pybamm.Interpolant(
+                [x], y, pybamm.t * timescale, name=name, interpolator=interpolator
+            )
+
+        pybamm_name = field.field_info.alias
+        pybamm_name_lower = pybamm_name[:1].lower() + pybamm_name[1:]
+        if pybamm_name.startswith("Initial concentration") or pybamm_name.startswith(
+            "Maximum concentration"
+        ):
+            init_len = len("Initial concentration ")
+            pybamm_name = (
+                pybamm_name[:init_len]
+                + "in "
+                + domain.pre_name.lower()
+                + pybamm_name[init_len:]
+            )
+        elif pybamm_name.startswith("Particle radius"):
+            pybamm_name = domain.short_pre_name + pybamm_name_lower
+        elif pybamm_name.startswith("OCP"):
+            pybamm_name = domain.pre_name + pybamm_name
+        elif pybamm_name.startswith("Cation transference number"):
+            pybamm_name = pybamm_name
+        elif domain.pre_name != "":
+            pybamm_name = domain.pre_name + pybamm_name_lower
+
+        pybamm_dict[pybamm_name] = value
+    return pybamm_dict
diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py
index 9454f1ab8..d8c575206 100644
--- a/pybamm/parameters/parameter_values.py
+++ b/pybamm/parameters/parameter_values.py
@@ -102,6 +102,39 @@ class ParameterValues:
         for citation in citations:
             pybamm.citations.register(citation)
 
+    @staticmethod
+    def create_from_bpx(filename, target_soc=1):
+        """
+        Parameters
+        ----------
+        filename: str
+            The filename of the bpx file
+        target_soc : float, optional
+            Target state of charge. Must be between 0 and 1. Default is 1.
+
+        Returns
+        -------
+        ParameterValues
+            A parameter values object with the parameters in the bpx file
+
+        """
+        if target_soc < 0 or target_soc > 1:
+            raise ValueError("Target SOC should be between 0 and 1")
+
+        from bpx import parse_bpx_file, get_electrode_concentrations
+        from .bpx import _bpx_to_param_dict
+
+        # parse bpx
+        bpx = parse_bpx_file(filename)
+        pybamm_dict = _bpx_to_param_dict(bpx)
+
+        # get initial concentrations based on SOC
+        c_n_init, c_p_init = get_electrode_concentrations(target_soc, bpx)
+        pybamm_dict["Initial concentration in negative electrode [mol.m-3]"] = c_n_init
+        pybamm_dict["Initial concentration in positive electrode [mol.m-3]"] = c_p_init
+
+        return pybamm.ParameterValues(pybamm_dict)
+
     def __getitem__(self, key):
         return self._dict_items[key]
 
diff --git a/requirements.txt b/requirements.txt
index 0988a947d..b98b6576c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,6 +9,7 @@ imageio>=2.9.0
 jupyter  # For example notebooks
 pybtex>=0.24.0
 sympy >= 1.8
+bpx
 # Note: Matplotlib is loaded for debug plots but to ensure pybamm runs
 # on systems without an attached display it should never be imported
 # outside of plot() methods.
diff --git a/setup.py b/setup.py
index 479fdfabd..f8257e478 100644
--- a/setup.py
+++ b/setup.py
@@ -200,6 +200,7 @@ setup(
         "jupyter",  # For example notebooks
         "pybtex>=0.24.0",
         "sympy>=1.8",
+        "bpx",
         # Note: Matplotlib is loaded for debug plots, but to ensure pybamm runs
         # on systems without an attached display, it should never be imported
         # outside of plot() methods.


Test patch:
diff --git a/tests/unit/test_parameters/test_bpx.py b/tests/unit/test_parameters/test_bpx.py
new file mode 100644
index 000000000..5aeca8696
--- /dev/null
+++ b/tests/unit/test_parameters/test_bpx.py
@@ -0,0 +1,195 @@
+#
+# Tests for the create_from_bpx function
+#
+
+import tempfile
+import unittest
+import json
+import pybamm
+import copy
+
+
+class TestBPX(unittest.TestCase):
+    def setUp(self):
+        self.base = {
+            "Header": {
+                "BPX": 1.0,
+                "Title": "Parametrisation example",
+                "Description": "Test",
+                "Model": "DFN",
+            },
+            "Parameterisation": {
+                "Cell": {
+                    "Ambient temperature [K]": 298.15,
+                    "Initial temperature [K]": 298.15,
+                    "Reference temperature [K]": 298.15,
+                    "Lower voltage cut-off [V]": 2.8,
+                    "Upper voltage cut-off [V]": 4.2,
+                    "Nominal cell capacity [A.h]": 12.5,
+                    "Specific heat capacity [J.K-1.kg-1]": 913,
+                    "Thermal conductivity [W.m-1.K-1]": 2,
+                    "Density [kg.m-3]": 1847,
+                    "Electrode area [m2]": 0.016808,
+                    "Number of electrode pairs connected "
+                    "in parallel to make a cell": 34,
+                    "External surface area [m2]": 3.79e-2,
+                    "Volume [m3]": 1.28e-4,
+                },
+                "Electrolyte": {
+                    "Initial concentration [mol.m-3]": 1000,
+                    "Cation transference number": 0.259,
+                    "Conductivity [S.m-1]": (
+                        "0.1297 * (x / 1000) ** 3 "
+                        "- 2.51 * (x / 1000) ** 1.5 + 3.329 * (x / 1000)"
+                    ),
+                    "Diffusivity [m2.s-1]": (
+                        "8.794e-11 * (x / 1000) ** 2 "
+                        "- 3.972e-10 * (x / 1000) + 4.862e-10"
+                    ),
+                    "Conductivity activation energy [J.mol-1]": 34200,
+                    "Diffusivity activation energy [J.mol-1]": 34200,
+                },
+                "Negative electrode": {
+                    "Particle radius [m]": 4.12e-06,
+                    "Thickness [m]": 5.62e-05,
+                    "Diffusivity [m2.s-1]": "8.6e-13 * exp(-13.5 * x) + 9.5e-15",
+                    "OCP [V]": (
+                        "7.84108819e-01 * exp(-5.16822591e01 * x) + "
+                        "5.99914745e02 + "
+                        "2.62306941e-02 * tanh(-1.71992993e01 * (x - 5.48687033e-01)) +"
+                        "9.41099327e02 * tanh(-6.91080049e-01 * (x + 2.49433043e00)) + "
+                        "3.40646063e02 * tanh(7.27243978e-01 * (x + 1.64297574e00))"
+                    ),
+                    "Entropic change coefficient [V.K-1]": "-0.001*x",
+                    "Conductivity [S.m-1]": 0.39,
+                    "Surface area per unit volume [m-1]": 487864,
+                    "Porosity": 0.33,
+                    "Transport efficiency": 0.19,
+                    "Reaction rate constant [mol.m-2.s-1]": 5.75e-06,
+                    "Minimum stoichiometry": 0.008536,
+                    "Maximum stoichiometry": 0.824874,
+                    "Maximum concentration [mol.m-3]": 28500,
+                    "Diffusivity activation energy [J.mol-1]": 108000,
+                    "Reaction rate constant activation energy [J.mol-1]": 53400,
+                },
+                "Positive electrode": {
+                    "Particle radius [m]": 4.6e-06,
+                    "Thickness [m]": 5.23e-05,
+                    "Diffusivity [m2.s-1]": (
+                        "5e-13 - x * 8e-14 - 4.1e-13 * exp(-12 * (x - 0.98) ** 2)"
+                    ),
+                    "OCP [V]": (
+                        "-2.59073509 * x + "
+                        "4.17659428 - "
+                        "11.03429916 * tanh(-9.343666 * (x - 0.79475919)) - "
+                        "1.63480454 * tanh(82.26606342 * (x - 1.00945121)) - "
+                        "10.70641562 * tanh(9.43939843 * (x - 0.79469384))"
+                    ),
+                    "Entropic change coefficient [V.K-1]": "-0.001*x",
+                    "Conductivity [S.m-1]": 1.464,
+                    "Surface area per unit volume [m-1]": 404348,
+                    "Porosity": 0.385,
+                    "Transport efficiency": 0.2389,
+                    "Reaction rate constant [mol.m-2.s-1]": 2.5e-05,
+                    "Minimum stoichiometry": 0.462455,
+                    "Maximum stoichiometry": 0.940627,
+                    "Maximum concentration [mol.m-3]": 56500,
+                    "Diffusivity activation energy [J.mol-1]": 62400,
+                    "Reaction rate constant activation energy [J.mol-1]": 27010,
+                },
+                "Separator": {
+                    "Thickness [m]": 2e-5,
+                    "Porosity": 0.47,
+                    "Transport efficiency": 0.3222,
+                },
+            },
+        }
+
+    def test_bpx(self):
+        bpx_obj = copy.copy(self.base)
+
+        filename = "tmp.json"
+        with tempfile.NamedTemporaryFile(
+            suffix=filename, delete=False, mode="w"
+        ) as tmp:
+            # write to a tempory file so we can
+            # get the source later on using inspect.getsource
+            # (as long as the file still exists)
+            json.dump(bpx_obj, tmp)
+            tmp.flush()
+
+            pv = pybamm.ParameterValues.create_from_bpx(tmp.name)
+
+            model = pybamm.lithium_ion.DFN()
+            experiment = pybamm.Experiment(
+                [
+                    "Discharge at C/5 for 1 hour",
+                ]
+            )
+            sim = pybamm.Simulation(model, parameter_values=pv, experiment=experiment)
+            sim.solve()
+
+    def test_constant_functions(self):
+        bpx_obj = copy.copy(self.base)
+        bpx_obj["Parameterisation"]["Electrolyte"].update(
+            {
+                "Conductivity [S.m-1]": 1,
+                "Diffusivity [m2.s-1]": 1,
+            }
+        )
+        bpx_obj["Parameterisation"]["Negative electrode"].update(
+            {
+                "Diffusivity [m2.s-1]": 1,
+                "Entropic change coefficient [V.K-1]": 1,
+            }
+        )
+        bpx_obj["Parameterisation"]["Positive electrode"].update(
+            {
+                "Diffusivity [m2.s-1]": 1,
+                "Entropic change coefficient [V.K-1]": 1,
+            }
+        )
+
+        filename = "tmp.json"
+        with tempfile.NamedTemporaryFile(
+            suffix=filename, delete=False, mode="w"
+        ) as tmp:
+            # write to a tempory file so we can
+            # get the source later on using inspect.getsource
+            # (as long as the file still exists)
+            json.dump(bpx_obj, tmp)
+            tmp.flush()
+
+            pybamm.ParameterValues.create_from_bpx(tmp.name)
+
+    def test_table_data(self):
+        bpx_obj = copy.copy(self.base)
+        bpx_obj["Parameterisation"]["Electrolyte"].update(
+            {"Conductivity [S.m-1]": {"x": [800, 1000, 1200], "y": [0.9, 1, 1.1]}}
+        )
+
+        filename = "tmp.json"
+        with tempfile.NamedTemporaryFile(
+            suffix=filename, delete=False, mode="w"
+        ) as tmp:
+            # write to a tempory file so we can
+            # get the source later on using inspect.getsource
+            # (as long as the file still exists)
+            json.dump(bpx_obj, tmp)
+            tmp.flush()
+
+            pybamm.ParameterValues.create_from_bpx(tmp.name)
+
+    def test_bpx_soc_error(self):
+        with self.assertRaisesRegex(ValueError, "Target SOC"):
+            pybamm.ParameterValues.create_from_bpx("blah.json", target_soc=10)
+
+
+if __name__ == "__main__":
+    print("Add -v for more debug output")
+    import sys
+
+    if "-v" in sys.argv:
+        debug = True
+    pybamm.settings.debug_mode = True
+    unittest.main()