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": [ + "02004006008003.73.723.743.763.78ReferenceModelOptimised ComparisonTime / sVoltage / V" + ] + }, + "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": [ + "510152025300.00050.00060.00070.00080.00090.001ConvergenceIterationCost" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "0501001500.450.50.550.60.650.70.750.80.850.90501001500.50.520.540.560.580.60.620.640.660.68Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + ] + }, + "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": [ + "0.50.60.70.80.90.50.550.60.650.70.750.80.040.080.120.160.20.24Cost LandscapeNegative electrode active material volume fractionPositive electrode active material volume fraction" + ] + }, + "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()