diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1453728d5..6230e05b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## Features
+- [#250](https://github.com/pybop-team/PyBOP/pull/250) - Adds DFN, MPM, MSMR models and moves multiple construction variables to BaseEChem. Adds exception catch on simulate & simulateS1.
- [#241](https://github.com/pybop-team/PyBOP/pull/241) - Adds experimental circuit model fitting notebook with LG M50 data.
- [#268](https://github.com/pybop-team/PyBOP/pull/268) - Fixes the GitHub Release artifact uploads, allowing verification of
codesigned binaries and source distributions via `sigstore-python`.
diff --git a/examples/notebooks/pouch_cell_identification.ipynb b/examples/notebooks/pouch_cell_identification.ipynb
new file mode 100644
index 000000000..41fc4faf7
--- /dev/null
+++ b/examples/notebooks/pouch_cell_identification.ipynb
@@ -0,0 +1,2710 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "expmkveO04pw"
+ },
+ "source": [
+ "## Pouch Cell Model Parameter Identification\n",
+ "\n",
+ "In this notebook, we present the single particle model with a two dimensional current collector. This is achieved via the potential-pair models introduced in [[1]](10.1149/1945-7111/abbce4) as implemented in PyBaMM. At a high-level this is accomplished as a potential-pair model which is resolved across the discretised spatial locations.\n",
+ "\n",
+ "### Setting up the Environment\n",
+ "\n",
+ "Before we begin, we need to ensure that we have all the necessary tools. We will install PyBOP from its development branch and upgrade some dependencies:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "X87NUGPW04py",
+ "outputId": "0d785b07-7cff-4aeb-e60a-4ff5a669afbf"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Requirement already satisfied: pip in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (24.0)\n",
+ "Requirement already satisfied: ipywidgets in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (8.1.2)\n",
+ "Requirement already satisfied: comm>=0.1.3 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (0.2.1)\n",
+ "Requirement already satisfied: ipython>=6.1.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (8.22.1)\n",
+ "Requirement already satisfied: traitlets>=4.3.1 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (5.14.1)\n",
+ "Requirement already satisfied: widgetsnbextension~=4.0.10 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (4.0.10)\n",
+ "Requirement already satisfied: jupyterlab-widgets~=3.0.10 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (3.0.10)\n",
+ "Requirement already satisfied: decorator in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (5.1.1)\n",
+ "Requirement already satisfied: jedi>=0.16 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.1)\n",
+ "Requirement already satisfied: matplotlib-inline in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.6)\n",
+ "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.43)\n",
+ "Requirement already satisfied: pygments>=2.4.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (2.17.2)\n",
+ "Requirement already satisfied: stack-data in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\n",
+ "Requirement already satisfied: pexpect>4.3 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\n",
+ "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.3)\n",
+ "Requirement already satisfied: ptyprocess>=0.5 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\n",
+ "Requirement already satisfied: wcwidth in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\n",
+ "Requirement already satisfied: executing>=1.2.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.0.1)\n",
+ "Requirement already satisfied: asttokens>=2.1.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.4.1)\n",
+ "Requirement already satisfied: pure-eval in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (0.2.2)\n",
+ "Requirement already satisfied: six>=1.12.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from asttokens>=2.1.0->stack-data->ipython>=6.1.0->ipywidgets) (1.16.0)\n",
+ "Note: you may need to restart the kernel to use updated packages.\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pip install --upgrade pip ipywidgets\n",
+ "%pip install pybop -q"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "jAvD5fk104p0"
+ },
+ "source": [
+ "### Importing Libraries\n",
+ "\n",
+ "With the environment set up, we can now import PyBOP alongside other libraries we will need:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "id": "SQdt4brD04p1"
+ },
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import plotly.graph_objects as go\n",
+ "\n",
+ "import pybop"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "5XU-dMtU04p2"
+ },
+ "source": [
+ "### Generate Synthetic Data\n",
+ "\n",
+ "To demonstrate parameter estimation, we first need some data. We will generate synthetic data using the PyBOP forward model, which requires defining a parameter set and the model itself.\n",
+ "\n",
+ "#### Defining Parameters and Model\n",
+ "\n",
+ "We start by creating an example parameter set and then instantiate the single-particle model (SPM):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "parameter_set = pybop.ParameterSet.pybamm(\"Marquis2019\")\n",
+ "parameter_set.update(\n",
+ " {\n",
+ " \"Negative electrode active material volume fraction\": 0.495,\n",
+ " \"Positive electrode active material volume fraction\": 0.612,\n",
+ " }\n",
+ ")\n",
+ "model = pybop.lithium_ion.SPM(\n",
+ " parameter_set=parameter_set,\n",
+ " options={\"current collector\": \"potential pair\", \"dimensionality\": 2},\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Update the number of spatial locations\n",
+ "\n",
+ "Next, we update the number of spatial locations to solve the potential-pair model,"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model.var_pts[\"y\"] = 5\n",
+ "model.var_pts[\"z\"] = 5"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Simulating Forward Model\n",
+ "\n",
+ "We can then simulate the model using the `predict` method, with a default constant current to generate voltage data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "id": "sBasxv8U04p3"
+ },
+ "outputs": [],
+ "source": [
+ "t_eval = np.arange(0, 900, 2)\n",
+ "values = model.predict(t_eval=t_eval)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Adding Noise to Voltage Data\n",
+ "\n",
+ "To make the parameter estimation more realistic, we add Gaussian noise to the data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sigma = 0.001\n",
+ "corrupt_values = values[\"Voltage [V]\"].data + np.random.normal(0, sigma, len(t_eval))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "X8-tubYY04p_"
+ },
+ "source": [
+ "## Identify the Parameters"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "PQqhvSZN04p_"
+ },
+ "source": [
+ "We will now set up the parameter estimation process by defining the datasets for optimisation and selecting the model parameters we wish to estimate."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Creating Optimisation Dataset\n",
+ "\n",
+ "The dataset for optimisation is composed of time, current, and the noisy voltage data:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "id": "zuvGHWID04p_"
+ },
+ "outputs": [],
+ "source": [
+ "dataset = pybop.Dataset(\n",
+ " {\n",
+ " \"Time [s]\": t_eval,\n",
+ " \"Current function [A]\": values[\"Current [A]\"].data,\n",
+ " \"Voltage [V]\": corrupt_values,\n",
+ " }\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "ffS3CF_704qA"
+ },
+ "source": [
+ "### Defining Parameters to Estimate\n",
+ "\n",
+ "We select the parameters for estimation and set up their prior distributions and bounds:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "id": "WPCybXIJ04qA"
+ },
+ "outputs": [],
+ "source": [
+ "parameters = [\n",
+ " pybop.Parameter(\n",
+ " \"Negative electrode active material volume fraction\",\n",
+ " prior=pybop.Gaussian(0.7, 0.05),\n",
+ " bounds=[0.45, 0.9],\n",
+ " ),\n",
+ " pybop.Parameter(\n",
+ " \"Positive electrode active material volume fraction\",\n",
+ " prior=pybop.Gaussian(0.58, 0.05),\n",
+ " bounds=[0.5, 0.8],\n",
+ " ),\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For plotting purposed, we want additional variables to be stored in the problem class. These are defined as,"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "additional_variables = [\n",
+ " \"Negative current collector potential [V]\",\n",
+ " \"Positive current collector potential [V]\",\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "n4OHa-aF04qA"
+ },
+ "source": [
+ "### Setting up the Optimisation Problem\n",
+ "\n",
+ "With the datasets and parameters defined, we can set up the optimisation problem, its cost function, and the optimiser."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "id": "etMzRtx404qA"
+ },
+ "outputs": [],
+ "source": [
+ "problem = pybop.FittingProblem(\n",
+ " model, parameters, dataset, additional_variables=additional_variables\n",
+ ")\n",
+ "cost = pybop.SumSquaredError(problem)\n",
+ "optim = pybop.Optimisation(cost, optimiser=pybop.CMAES)\n",
+ "optim.set_max_iterations(30)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "caprp-bV04qB"
+ },
+ "source": [
+ "### Running the Optimisation\n",
+ "\n",
+ "We proceed to run the CMA-ES optimisation algorithm to estimate the parameters:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "id": "-9OVt0EQ04qB"
+ },
+ "outputs": [],
+ "source": [
+ "x, final_cost = optim.run()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "-4pZsDmS04qC"
+ },
+ "source": [
+ "### Viewing the Estimated Parameters\n",
+ "\n",
+ "After the optimisation, we can examine the estimated parameter values:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "Hgz8SV4i04qC",
+ "outputId": "e1e42ae7-5075-4c47-dd68-1b22ecc170f6"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([0.47496537, 0.61140011])"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "x # This will output the estimated parameters"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "KxKURtH704qC"
+ },
+ "source": [
+ "## Plotting and Visualisation\n",
+ "\n",
+ "PyBOP provides various plotting utilities to visualise the results of the optimisation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "-cWCOiqR04qC"
+ },
+ "source": [
+ "### Comparing System Response\n",
+ "\n",
+ "We can quickly plot the system's response using the estimated parameters compared to the target:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 467
+ },
+ "id": "tJUJ80Ve04qD",
+ "outputId": "855fbaa2-1e09-4935-eb1a-8caf7f99eb75"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "pybop.quick_plot(problem, parameter_values=x, title=\"Optimised Comparison\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Spatial Plotting\n",
+ "\n",
+ "We can now plot the spatial variables from the solution object. First, the final negative current collector potential can be displayed. In this example, this is just a reference variable, but could be used for fitting or optimisation in the correct workflows."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "colorscale": [
+ [
+ 0,
+ "#440154"
+ ],
+ [
+ 0.1111111111111111,
+ "#482878"
+ ],
+ [
+ 0.2222222222222222,
+ "#3e4989"
+ ],
+ [
+ 0.3333333333333333,
+ "#31688e"
+ ],
+ [
+ 0.4444444444444444,
+ "#26828e"
+ ],
+ [
+ 0.5555555555555556,
+ "#1f9e89"
+ ],
+ [
+ 0.6666666666666666,
+ "#35b779"
+ ],
+ [
+ 0.7777777777777778,
+ "#6ece58"
+ ],
+ [
+ 0.8888888888888888,
+ "#b5de2b"
+ ],
+ [
+ 1,
+ "#fde725"
+ ]
+ ],
+ "type": "contour",
+ "x": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "y": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "z": [
+ [
+ -0.00022055019399999868,
+ -0.00020817991511510437,
+ -0.00017733967875949684,
+ -0.00013382475248570048,
+ -0.00009744280975082956
+ ],
+ [
+ -0.0002200682921755627,
+ -0.0002077164608979826,
+ -0.00017025065441730902,
+ -0.000104111854657049,
+ 3.792828462001579e-30
+ ],
+ [
+ -0.00023289339271472128,
+ -0.00022148992831166653,
+ -0.0001854997008337834,
+ -0.00011792954324305566,
+ 9.13516546016691e-32
+ ],
+ [
+ -0.0002546876903842077,
+ -0.00024828747219895466,
+ -0.0002297088191867596,
+ -0.000203527233570664,
+ -0.0001826834881019309
+ ],
+ [
+ -0.00026260492784945276,
+ -0.0002597799801717452,
+ -0.0002481422113351371,
+ -0.00023376411978310373,
+ -0.00022697336267411963
+ ]
+ ]
+ }
+ ],
+ "layout": {
+ "height": 600,
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "heatmapgl": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmapgl"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "text": "Negative current collector potential [V]"
+ },
+ "width": 600,
+ "xaxis": {
+ "title": {
+ "text": "x node"
+ }
+ },
+ "yaxis": {
+ "title": {
+ "text": "y node"
+ }
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "sol = problem.evaluate(x)\n",
+ "\n",
+ "go.Figure(\n",
+ " [\n",
+ " go.Contour(\n",
+ " x=np.arange(0, model.var_pts[\"y\"] - 1, 1),\n",
+ " y=np.arange(0, model.var_pts[\"z\"] - 1, 1),\n",
+ " z=sol[\"Negative current collector potential [V]\"][:, :, -1],\n",
+ " colorscale=\"Viridis\",\n",
+ " )\n",
+ " ],\n",
+ " layout=dict(\n",
+ " title=\"Negative current collector potential [V]\",\n",
+ " xaxis_title=\"x node\",\n",
+ " yaxis_title=\"y node\",\n",
+ " width=600,\n",
+ " height=600,\n",
+ " ),\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We plot can then plot the positive current collector potential,"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "colorscale": [
+ [
+ 0,
+ "#440154"
+ ],
+ [
+ 0.1111111111111111,
+ "#482878"
+ ],
+ [
+ 0.2222222222222222,
+ "#3e4989"
+ ],
+ [
+ 0.3333333333333333,
+ "#31688e"
+ ],
+ [
+ 0.4444444444444444,
+ "#26828e"
+ ],
+ [
+ 0.5555555555555556,
+ "#1f9e89"
+ ],
+ [
+ 0.6666666666666666,
+ "#35b779"
+ ],
+ [
+ 0.7777777777777778,
+ "#6ece58"
+ ],
+ [
+ 0.8888888888888888,
+ "#b5de2b"
+ ],
+ [
+ 1,
+ "#fde725"
+ ]
+ ],
+ "type": "contour",
+ "x": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "y": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "z": [
+ [
+ 3.7078876177015263,
+ 3.707876347847289,
+ 3.707853323934703,
+ 3.707826016404782,
+ 3.7078085641197807
+ ],
+ [
+ 3.70786512440276,
+ 3.707853590894792,
+ 3.7078220221903138,
+ 3.707778552309027,
+ 3.707744572139743
+ ],
+ [
+ 3.7078229486185177,
+ 3.707804218479872,
+ 3.707745495045241,
+ 3.70763696229213,
+ 3.7074529354654655
+ ],
+ [
+ 3.7077939368814823,
+ 3.7077737787683542,
+ 3.7077102612801465,
+ 3.7075952142137254,
+ 3.707407186787417
+ ],
+ [
+ 3.7077846062571647,
+ 3.70776995420114,
+ 3.707720253036077,
+ 3.707647593021621,
+ 3.7075890859257985
+ ]
+ ]
+ }
+ ],
+ "layout": {
+ "height": 600,
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "heatmapgl": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmapgl"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "text": "Positive current collector potential [V]"
+ },
+ "width": 600,
+ "xaxis": {
+ "title": {
+ "text": "x node"
+ }
+ },
+ "yaxis": {
+ "title": {
+ "text": "y node"
+ }
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "go.Figure(\n",
+ " [\n",
+ " go.Contour(\n",
+ " x=np.arange(0, model.var_pts[\"y\"] - 1, 1),\n",
+ " y=np.arange(0, model.var_pts[\"z\"] - 1, 1),\n",
+ " z=sol[\"Positive current collector potential [V]\"][:, :, -1],\n",
+ " colorscale=\"Viridis\",\n",
+ " )\n",
+ " ],\n",
+ " layout=dict(\n",
+ " title=\"Positive current collector potential [V]\",\n",
+ " xaxis_title=\"x node\",\n",
+ " yaxis_title=\"y node\",\n",
+ " width=600,\n",
+ " height=600,\n",
+ " ),\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Convergence and Parameter Trajectories\n",
+ "\n",
+ "To assess the optimisation process, we can plot the convergence of the cost function and the trajectories of the parameters:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {
+ "id": "N5XYkevi04qD"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "pybop.plot_convergence(optim)\n",
+ "pybop.plot_parameters(optim);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Cost Landscape\n",
+ "\n",
+ "Finally, we can visualise the cost landscape and the path taken by the optimiser:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "pybop.plot2d(optim, steps=15);"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.7"
+ },
+ "widgets": {
+ "application/vnd.jupyter.widget-state+json": {
+ "06f2374f91c8455bb63252092512f2ed": {
+ "model_module": "@jupyter-widgets/base",
+ "model_module_version": "2.0.0",
+ "model_name": "LayoutModel",
+ "state": {
+ "_model_module": "@jupyter-widgets/base",
+ "_model_module_version": "2.0.0",
+ "_model_name": "LayoutModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/base",
+ "_view_module_version": "2.0.0",
+ "_view_name": "LayoutView",
+ "align_content": null,
+ "align_items": null,
+ "align_self": null,
+ "border_bottom": null,
+ "border_left": null,
+ "border_right": null,
+ "border_top": null,
+ "bottom": null,
+ "display": null,
+ "flex": null,
+ "flex_flow": null,
+ "grid_area": null,
+ "grid_auto_columns": null,
+ "grid_auto_flow": null,
+ "grid_auto_rows": null,
+ "grid_column": null,
+ "grid_gap": null,
+ "grid_row": null,
+ "grid_template_areas": null,
+ "grid_template_columns": null,
+ "grid_template_rows": null,
+ "height": null,
+ "justify_content": null,
+ "justify_items": null,
+ "left": null,
+ "margin": null,
+ "max_height": null,
+ "max_width": null,
+ "min_height": null,
+ "min_width": null,
+ "object_fit": null,
+ "object_position": null,
+ "order": null,
+ "overflow": null,
+ "padding": null,
+ "right": null,
+ "top": null,
+ "visibility": null,
+ "width": null
+ }
+ },
+ "423bffea3a1c42b49a9ad71218e5811b": {
+ "model_module": "@jupyter-widgets/base",
+ "model_module_version": "2.0.0",
+ "model_name": "LayoutModel",
+ "state": {
+ "_model_module": "@jupyter-widgets/base",
+ "_model_module_version": "2.0.0",
+ "_model_name": "LayoutModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/base",
+ "_view_module_version": "2.0.0",
+ "_view_name": "LayoutView",
+ "align_content": null,
+ "align_items": null,
+ "align_self": null,
+ "border_bottom": null,
+ "border_left": null,
+ "border_right": null,
+ "border_top": null,
+ "bottom": null,
+ "display": null,
+ "flex": null,
+ "flex_flow": null,
+ "grid_area": null,
+ "grid_auto_columns": null,
+ "grid_auto_flow": null,
+ "grid_auto_rows": null,
+ "grid_column": null,
+ "grid_gap": null,
+ "grid_row": null,
+ "grid_template_areas": null,
+ "grid_template_columns": null,
+ "grid_template_rows": null,
+ "height": null,
+ "justify_content": null,
+ "justify_items": null,
+ "left": null,
+ "margin": null,
+ "max_height": null,
+ "max_width": null,
+ "min_height": null,
+ "min_width": null,
+ "object_fit": null,
+ "object_position": null,
+ "order": null,
+ "overflow": null,
+ "padding": null,
+ "right": null,
+ "top": null,
+ "visibility": null,
+ "width": null
+ }
+ },
+ "56ff19291e464d63b23e63b8e2ac9ea3": {
+ "model_module": "@jupyter-widgets/controls",
+ "model_module_version": "2.0.0",
+ "model_name": "SliderStyleModel",
+ "state": {
+ "_model_module": "@jupyter-widgets/controls",
+ "_model_module_version": "2.0.0",
+ "_model_name": "SliderStyleModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/base",
+ "_view_module_version": "2.0.0",
+ "_view_name": "StyleView",
+ "description_width": "",
+ "handle_color": null
+ }
+ },
+ "646a8670cb204a31bb56bc2380898093": {
+ "model_module": "@jupyter-widgets/base",
+ "model_module_version": "2.0.0",
+ "model_name": "LayoutModel",
+ "state": {
+ "_model_module": "@jupyter-widgets/base",
+ "_model_module_version": "2.0.0",
+ "_model_name": "LayoutModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/base",
+ "_view_module_version": "2.0.0",
+ "_view_name": "LayoutView",
+ "align_content": null,
+ "align_items": null,
+ "align_self": null,
+ "border_bottom": null,
+ "border_left": null,
+ "border_right": null,
+ "border_top": null,
+ "bottom": null,
+ "display": null,
+ "flex": null,
+ "flex_flow": null,
+ "grid_area": null,
+ "grid_auto_columns": null,
+ "grid_auto_flow": null,
+ "grid_auto_rows": null,
+ "grid_column": null,
+ "grid_gap": null,
+ "grid_row": null,
+ "grid_template_areas": null,
+ "grid_template_columns": null,
+ "grid_template_rows": null,
+ "height": null,
+ "justify_content": null,
+ "justify_items": null,
+ "left": null,
+ "margin": null,
+ "max_height": null,
+ "max_width": null,
+ "min_height": null,
+ "min_width": null,
+ "object_fit": null,
+ "object_position": null,
+ "order": null,
+ "overflow": null,
+ "padding": null,
+ "right": null,
+ "top": null,
+ "visibility": null,
+ "width": null
+ }
+ },
+ "7d46516469314b88be3500e2afcafcf6": {
+ "model_module": "@jupyter-widgets/output",
+ "model_module_version": "1.0.0",
+ "model_name": "OutputModel",
+ "state": {
+ "_dom_classes": [],
+ "_model_module": "@jupyter-widgets/output",
+ "_model_module_version": "1.0.0",
+ "_model_name": "OutputModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/output",
+ "_view_module_version": "1.0.0",
+ "_view_name": "OutputView",
+ "layout": "IPY_MODEL_646a8670cb204a31bb56bc2380898093",
+ "msg_id": "",
+ "outputs": [],
+ "tabbable": null,
+ "tooltip": null
+ }
+ },
+ "8d003c14da5f4fa68284b28c15cee6e6": {
+ "model_module": "@jupyter-widgets/controls",
+ "model_module_version": "2.0.0",
+ "model_name": "VBoxModel",
+ "state": {
+ "_dom_classes": [
+ "widget-interact"
+ ],
+ "_model_module": "@jupyter-widgets/controls",
+ "_model_module_version": "2.0.0",
+ "_model_name": "VBoxModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/controls",
+ "_view_module_version": "2.0.0",
+ "_view_name": "VBoxView",
+ "box_style": "",
+ "children": [
+ "IPY_MODEL_aef2fa7adcc14ad0854b73d5910ae3b4",
+ "IPY_MODEL_7d46516469314b88be3500e2afcafcf6"
+ ],
+ "layout": "IPY_MODEL_423bffea3a1c42b49a9ad71218e5811b",
+ "tabbable": null,
+ "tooltip": null
+ }
+ },
+ "aef2fa7adcc14ad0854b73d5910ae3b4": {
+ "model_module": "@jupyter-widgets/controls",
+ "model_module_version": "2.0.0",
+ "model_name": "FloatSliderModel",
+ "state": {
+ "_dom_classes": [],
+ "_model_module": "@jupyter-widgets/controls",
+ "_model_module_version": "2.0.0",
+ "_model_name": "FloatSliderModel",
+ "_view_count": null,
+ "_view_module": "@jupyter-widgets/controls",
+ "_view_module_version": "2.0.0",
+ "_view_name": "FloatSliderView",
+ "behavior": "drag-tap",
+ "continuous_update": true,
+ "description": "t",
+ "description_allow_html": false,
+ "disabled": false,
+ "layout": "IPY_MODEL_06f2374f91c8455bb63252092512f2ed",
+ "max": 1.1333333333333333,
+ "min": 0,
+ "orientation": "horizontal",
+ "readout": true,
+ "readout_format": ".2f",
+ "step": 0.011333333333333332,
+ "style": "IPY_MODEL_56ff19291e464d63b23e63b8e2ac9ea3",
+ "tabbable": null,
+ "tooltip": null,
+ "value": 0
+ }
+ }
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/pybop/models/base_model.py b/pybop/models/base_model.py
index ba5701709..239a90224 100644
--- a/pybop/models/base_model.py
+++ b/pybop/models/base_model.py
@@ -355,9 +355,13 @@ def simulate(self, inputs, t_eval) -> np.ndarray[np.float64]:
inputs=inputs,
allow_infeasible_solutions=self.allow_infeasible_solutions,
):
- sol = self.solver.solve(
- self.built_model, inputs=inputs, t_eval=t_eval
- )
+ try:
+ sol = self.solver.solve(
+ self.built_model, inputs=inputs, t_eval=t_eval
+ )
+ except Exception as e:
+ print(f"Error: {e}")
+ return [np.inf]
else:
return {signal: [np.inf] for signal in self.signal}
@@ -407,33 +411,37 @@ def simulateS1(self, inputs, t_eval):
inputs=inputs,
allow_infeasible_solutions=self.allow_infeasible_solutions,
):
- sol = self._solver.solve(
- self.built_model,
- inputs=inputs,
- t_eval=t_eval,
- calculate_sensitivities=True,
- )
- y = {signal: sol[signal].data for signal in self.signal}
-
- # Extract the sensitivities and stack them along a new axis for each signal
- dy = np.empty(
- (
- sol[self.signal[0]].data.shape[0],
- self.n_outputs,
- self.n_parameters,
+ try:
+ sol = self._solver.solve(
+ self.built_model,
+ inputs=inputs,
+ t_eval=t_eval,
+ calculate_sensitivities=True,
)
- )
-
- for i, signal in enumerate(self.signal):
- dy[:, i, :] = np.stack(
- [
- sol[signal].sensitivities[key].toarray()[:, 0]
- for key in self.fit_keys
- ],
- axis=-1,
+ y = {signal: sol[signal].data for signal in self.signal}
+
+ # Extract the sensitivities and stack them along a new axis for each signal
+ dy = np.empty(
+ (
+ sol[self.signal[0]].data.shape[0],
+ self.n_outputs,
+ self.n_parameters,
+ )
)
- return y, dy
+ for i, signal in enumerate(self.signal):
+ dy[:, i, :] = np.stack(
+ [
+ sol[signal].sensitivities[key].toarray()[:, 0]
+ for key in self.fit_keys
+ ],
+ axis=-1,
+ )
+
+ return y, dy
+ except Exception as e:
+ print(f"Error: {e}")
+ return [np.inf], [np.inf]
else:
return {signal: [np.inf] for signal in self.signal}, [np.inf]
diff --git a/pybop/models/empirical/__init__.py b/pybop/models/empirical/__init__.py
index 6a28b0a98..46f8a3734 100644
--- a/pybop/models/empirical/__init__.py
+++ b/pybop/models/empirical/__init__.py
@@ -1,4 +1,5 @@
#
# Import lithium ion based models
#
-from .ecm import ECircuitModel, Thevenin
+from .base_ecm import ECircuitModel
+from .ecm import Thevenin
diff --git a/pybop/models/empirical/ecm_base.py b/pybop/models/empirical/base_ecm.py
similarity index 100%
rename from pybop/models/empirical/ecm_base.py
rename to pybop/models/empirical/base_ecm.py
diff --git a/pybop/models/empirical/ecm.py b/pybop/models/empirical/ecm.py
index 4d7290dfc..beac4956c 100644
--- a/pybop/models/empirical/ecm.py
+++ b/pybop/models/empirical/ecm.py
@@ -1,6 +1,6 @@
import pybamm
-from .ecm_base import ECircuitModel
+from .base_ecm import ECircuitModel
class Thevenin(ECircuitModel):
diff --git a/pybop/models/lithium_ion/__init__.py b/pybop/models/lithium_ion/__init__.py
index 4dca05ea0..80e2d2cc2 100644
--- a/pybop/models/lithium_ion/__init__.py
+++ b/pybop/models/lithium_ion/__init__.py
@@ -1,4 +1,5 @@
#
# Import lithium ion based models
#
-from .echem import EChemBaseModel, SPM, SPMe
+from .base_echem import EChemBaseModel
+from .echem import SPM, SPMe, DFN, MPM, MSMR
diff --git a/pybop/models/lithium_ion/echem_base.py b/pybop/models/lithium_ion/base_echem.py
similarity index 85%
rename from pybop/models/lithium_ion/echem_base.py
rename to pybop/models/lithium_ion/base_echem.py
index 677623d66..595f52935 100644
--- a/pybop/models/lithium_ion/echem_base.py
+++ b/pybop/models/lithium_ion/base_echem.py
@@ -1,5 +1,7 @@
import warnings
+import pybamm
+
from ..base_model import BaseModel
@@ -8,8 +10,44 @@ class EChemBaseModel(BaseModel):
Overwrites and extends `BaseModel` class for electrochemical PyBaMM models.
"""
- def __init__(self, name, parameter_set):
- super().__init__(name, parameter_set)
+ def __init__(
+ self,
+ model,
+ name="Electrochemical Base Model",
+ parameter_set=None,
+ geometry=None,
+ submesh_types=None,
+ var_pts=None,
+ spatial_methods=None,
+ solver=None,
+ ):
+ super().__init__(name=name, parameter_set=parameter_set)
+ self.pybamm_model = model
+
+ # Set parameters, using either the provided ones or the default
+ self.default_parameter_values = self.pybamm_model.default_parameter_values
+ self._parameter_set = self._parameter_set or self.default_parameter_values
+ self._unprocessed_parameter_set = self._parameter_set
+
+ # Define model geometry and discretization
+ self.geometry = geometry or self.pybamm_model.default_geometry
+ self.submesh_types = submesh_types or self.pybamm_model.default_submesh_types
+ self.var_pts = var_pts or self.pybamm_model.default_var_pts
+ self.spatial_methods = (
+ spatial_methods or self.pybamm_model.default_spatial_methods
+ )
+ self.solver = solver or self.pybamm_model.default_solver
+ self.solver.max_step_decrease_count = 1
+
+ # Internal attributes for the built model are initialized but not set
+ self._model_with_set_params = None
+ self._built_model = None
+ self._built_initial_soc = None
+ self._mesh = None
+ self._disc = None
+
+ self._electrode_soh = pybamm.lithium_ion.electrode_soh
+ self.rebuild_parameters = self.set_rebuild_parameters()
def _check_params(
self, inputs=None, parameter_set=None, allow_infeasible_solutions=True
diff --git a/pybop/models/lithium_ion/echem.py b/pybop/models/lithium_ion/echem.py
index c8ad861d1..30e9e6d00 100644
--- a/pybop/models/lithium_ion/echem.py
+++ b/pybop/models/lithium_ion/echem.py
@@ -1,6 +1,6 @@
import pybamm
-from .echem_base import EChemBaseModel
+from .base_echem import EChemBaseModel
class SPM(EChemBaseModel):
@@ -41,33 +41,19 @@ def __init__(
solver=None,
options=None,
):
- super().__init__(name, parameter_set)
self.pybamm_model = pybamm.lithium_ion.SPM(options=options)
self._unprocessed_model = self.pybamm_model
- # Set parameters, using either the provided ones or the default
- self.default_parameter_values = self.pybamm_model.default_parameter_values
- self._parameter_set = self._parameter_set or self.default_parameter_values
- self._unprocessed_parameter_set = self._parameter_set
-
- # Define model geometry and discretization
- self.geometry = geometry or self.pybamm_model.default_geometry
- self.submesh_types = submesh_types or self.pybamm_model.default_submesh_types
- self.var_pts = var_pts or self.pybamm_model.default_var_pts
- self.spatial_methods = (
- spatial_methods or self.pybamm_model.default_spatial_methods
+ super().__init__(
+ model=self.pybamm_model,
+ name=name,
+ parameter_set=parameter_set,
+ geometry=geometry,
+ submesh_types=submesh_types,
+ var_pts=var_pts,
+ spatial_methods=spatial_methods,
+ solver=solver,
)
- self.solver = solver or self.pybamm_model.default_solver
-
- # Internal attributes for the built model are initialized but not set
- self._model_with_set_params = None
- self._built_model = None
- self._built_initial_soc = None
- self._mesh = None
- self._disc = None
-
- self._electrode_soh = pybamm.lithium_ion.electrode_soh
- self.rebuild_parameters = self.set_rebuild_parameters()
class SPMe(EChemBaseModel):
@@ -110,30 +96,177 @@ def __init__(
solver=None,
options=None,
):
- super().__init__(name, parameter_set)
self.pybamm_model = pybamm.lithium_ion.SPMe(options=options)
self._unprocessed_model = self.pybamm_model
- # Set parameters, using either the provided ones or the default
- self.default_parameter_values = self.pybamm_model.default_parameter_values
- self._parameter_set = self._parameter_set or self.default_parameter_values
- self._unprocessed_parameter_set = self._parameter_set
-
- # Define model geometry and discretization
- self.geometry = geometry or self.pybamm_model.default_geometry
- self.submesh_types = submesh_types or self.pybamm_model.default_submesh_types
- self.var_pts = var_pts or self.pybamm_model.default_var_pts
- self.spatial_methods = (
- spatial_methods or self.pybamm_model.default_spatial_methods
+ super().__init__(
+ model=self.pybamm_model,
+ name=name,
+ parameter_set=parameter_set,
+ geometry=geometry,
+ submesh_types=submesh_types,
+ var_pts=var_pts,
+ spatial_methods=spatial_methods,
+ solver=solver,
+ )
+
+
+class DFN(EChemBaseModel):
+ """
+ Wraps the Doyle-Fuller-Newman (DFN) model for simulating lithium-ion batteries, as implemented in PyBaMM.
+
+ The DFN represents lithium-ion battery dynamics using multiple spherical particles
+ to simulate the behavior of the negative and positive electrodes. This model includes
+ electrolyte dynamics, solid-phase diffusion, and Butler-Volmer kinetics. This model
+ is the full-order representation used to reduce to the SPM, and SPMe models.
+
+ Parameters
+ ----------
+ name : str, optional
+ The name for the model instance, defaulting to "Doyle-Fuller-Newman".
+ parameter_set : pybamm.ParameterValues or dict, optional
+ The parameters for the model. If None, default parameters provided by PyBaMM are used.
+ geometry : dict, optional
+ The geometry definitions for the model. If None, default geometry from PyBaMM is used.
+ submesh_types : dict, optional
+ The types of submeshes to use. If None, default submesh types from PyBaMM are used.
+ var_pts : dict, optional
+ The discretization points for each variable in the model. If None, default points from PyBaMM are used.
+ spatial_methods : dict, optional
+ The spatial methods used for discretization. If None, default spatial methods from PyBaMM are used.
+ solver : pybamm.Solver, optional
+ The solver to use for simulating the model. If None, the default solver from PyBaMM is used.
+ options : dict, optional
+ A dictionary of options to customize the behavior of the PyBaMM model.
+ """
+
+ def __init__(
+ self,
+ name="Doyle-Fuller-Newman",
+ parameter_set=None,
+ geometry=None,
+ submesh_types=None,
+ var_pts=None,
+ spatial_methods=None,
+ solver=None,
+ options=None,
+ ):
+ self.pybamm_model = pybamm.lithium_ion.DFN(options=options)
+ self._unprocessed_model = self.pybamm_model
+
+ super().__init__(
+ model=self.pybamm_model,
+ name=name,
+ parameter_set=parameter_set,
+ geometry=geometry,
+ submesh_types=submesh_types,
+ var_pts=var_pts,
+ spatial_methods=spatial_methods,
+ solver=solver,
)
- self.solver = solver or self.pybamm_model.default_solver
- # Internal attributes for the built model are initialized but not set
- self._model_with_set_params = None
- self._built_model = None
- self._built_initial_soc = None
- self._mesh = None
- self._disc = None
- self._electrode_soh = pybamm.lithium_ion.electrode_soh
- self.rebuild_parameters = self.set_rebuild_parameters()
+class MPM(EChemBaseModel):
+ """
+ Wraps the Multi-Particle-Model (MPM) model for simulating lithium-ion batteries, as implemented in PyBaMM.
+
+ The MPM represents lithium-ion battery dynamics using a distribution of spherical particles
+ for each electrode. This model inherits the SPM class.
+
+ Parameters
+ ----------
+ name : str, optional
+ The name for the model instance, defaulting to "Many Particle Model".
+ parameter_set : pybamm.ParameterValues or dict, optional
+ The parameters for the model. If None, default parameters provided by PyBaMM are used.
+ geometry : dict, optional
+ The geometry definitions for the model. If None, default geometry from PyBaMM is used.
+ submesh_types : dict, optional
+ The types of submeshes to use. If None, default submesh types from PyBaMM are used.
+ var_pts : dict, optional
+ The discretization points for each variable in the model. If None, default points from PyBaMM are used.
+ spatial_methods : dict, optional
+ The spatial methods used for discretization. If None, default spatial methods from PyBaMM are used.
+ solver : pybamm.Solver, optional
+ The solver to use for simulating the model. If None, the default solver from PyBaMM is used.
+ options : dict, optional
+ A dictionary of options to customize the behavior of the PyBaMM model.
+ """
+
+ def __init__(
+ self,
+ name="Many Particle Model",
+ parameter_set=None,
+ geometry=None,
+ submesh_types=None,
+ var_pts=None,
+ spatial_methods=None,
+ solver=None,
+ options=None,
+ ):
+ self.pybamm_model = pybamm.lithium_ion.MPM(options=options)
+ self._unprocessed_model = self.pybamm_model
+
+ super().__init__(
+ model=self.pybamm_model,
+ name=name,
+ parameter_set=parameter_set,
+ geometry=geometry,
+ submesh_types=submesh_types,
+ var_pts=var_pts,
+ spatial_methods=spatial_methods,
+ solver=solver,
+ )
+
+
+class MSMR(EChemBaseModel):
+ """
+ Wraps the Multi-Species-Multi-Reactions (MSMR) model for simulating lithium-ion batteries, as implemented in PyBaMM.
+
+ The MSMR represents lithium-ion battery dynamics using a distribution of spherical particles for each electrode.
+ This model inherits the DFN class.
+
+ Parameters
+ ----------
+ name : str, optional
+ The name for the model instance, defaulting to "Multi Species Multi Reactions Model".
+ parameter_set : pybamm.ParameterValues or dict, optional
+ The parameters for the model. If None, default parameters provided by PyBaMM are used.
+ geometry : dict, optional
+ The geometry definitions for the model. If None, default geometry from PyBaMM is used.
+ submesh_types : dict, optional
+ The types of submeshes to use. If None, default submesh types from PyBaMM are used.
+ var_pts : dict, optional
+ The discretization points for each variable in the model. If None, default points from PyBaMM are used.
+ spatial_methods : dict, optional
+ The spatial methods used for discretization. If None, default spatial methods from PyBaMM are used.
+ solver : pybamm.Solver, optional
+ The solver to use for simulating the model. If None, the default solver from PyBaMM is used.
+ options : dict, optional
+ A dictionary of options to customize the behavior of the PyBaMM model.
+ """
+
+ def __init__(
+ self,
+ name="Multi Species Multi Reactions Model",
+ parameter_set=None,
+ geometry=None,
+ submesh_types=None,
+ var_pts=None,
+ spatial_methods=None,
+ solver=None,
+ options=None,
+ ):
+ self.pybamm_model = pybamm.lithium_ion.MSMR(options=options)
+ self._unprocessed_model = self.pybamm_model
+
+ super().__init__(
+ model=self.pybamm_model,
+ name=name,
+ parameter_set=parameter_set,
+ geometry=geometry,
+ submesh_types=submesh_types,
+ var_pts=var_pts,
+ spatial_methods=spatial_methods,
+ solver=solver,
+ )
diff --git a/pyproject.toml b/pyproject.toml
index df1b1ab9e..3663ba670 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,7 +61,10 @@ dev = [
"pytest-xdist",
"ruff",
]
-all = ["pybop[plot]"]
+scifem = [
+ "scikit-fem>=8.1.0" # scikit-fem is a dependency for the multi-dimensional pybamm models
+]
+all = ["pybop[plot]", "pybop[scifem]"]
[tool.setuptools.packages.find]
include = ["pybop", "pybop.*"]
diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py
index a4b5cfeaf..06783dbc9 100644
--- a/tests/unit/test_models.py
+++ b/tests/unit/test_models.py
@@ -15,6 +15,9 @@ class TestModels:
params=[
pybop.lithium_ion.SPM(),
pybop.lithium_ion.SPMe(),
+ pybop.lithium_ion.DFN(),
+ pybop.lithium_ion.MPM(),
+ pybop.lithium_ion.MSMR(options={"number of MSMR reactions": ("6", "4")}),
pybop.empirical.Thevenin(),
]
)
@@ -45,7 +48,7 @@ def test_predict_without_pybamm(self, model):
def test_predict_with_inputs(self, model):
# Define inputs
t_eval = np.linspace(0, 10, 100)
- if isinstance(model, (pybop.lithium_ion.SPM, pybop.lithium_ion.SPMe)):
+ if isinstance(model, (pybop.lithium_ion.EChemBaseModel)):
inputs = {
"Negative electrode active material volume fraction": 0.52,
"Positive electrode active material volume fraction": 0.63,
@@ -240,3 +243,31 @@ def test_basemodel(self):
with pytest.raises(NotImplementedError):
base.approximate_capacity(x)
+
+ @pytest.mark.unit
+ def test_non_converged_solution(self):
+ model = pybop.lithium_ion.DFN()
+ parameters = [
+ pybop.Parameter(
+ "Negative electrode active material volume fraction",
+ prior=pybop.Gaussian(0.2, 0.01),
+ ),
+ pybop.Parameter(
+ "Positive electrode active material volume fraction",
+ prior=pybop.Gaussian(0.2, 0.01),
+ ),
+ ]
+ dataset = pybop.Dataset(
+ {
+ "Time [s]": np.linspace(0, 100, 100),
+ "Current function [A]": np.zeros(100),
+ "Voltage [V]": np.zeros(100),
+ }
+ )
+
+ problem = pybop.FittingProblem(model, parameters=parameters, dataset=dataset)
+ res = problem.evaluate([-0.2, -0.2])
+ res_grad = problem.evaluateS1([-0.2, -0.2])
+
+ assert np.isinf(res).any()
+ assert np.isinf(res_grad).any()