{ "cells": [ { "cell_type": "markdown", "id": "00940c64-4748-4b08-9a35-ea98ce311e71", "metadata": {}, "source": [ "# Equivalent Circuit Parameter Identification\n", "\n", "This notebook provides example usage for identifying stationary parameters for a two RC branch Thevenin model. The Thevenin model represents an electrochemical battery through an empirical circuit model capable of capturing the electrical response of the battery. This model can be extended with a thermal submodel, as well as additional parallel resistor-capacitor branches.\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, "id": "dd0e1a20-1ba3-4ff5-8f6a-f9c6f25c2a4a", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:35.622147Z", "iopub.status.busy": "2024-04-14T18:57:35.621660Z", "iopub.status.idle": "2024-04-14T18:57:40.849137Z", "shell.execute_reply": "2024-04-14T18:57:40.848620Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: pip in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (24.0)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: ipywidgets in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (8.1.2)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: comm>=0.1.3 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (0.2.2)\r\n", "Requirement already satisfied: ipython>=6.1.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (8.23.0)\r\n", "Requirement already satisfied: traitlets>=4.3.1 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (5.14.2)\r\n", "Requirement already satisfied: widgetsnbextension~=4.0.10 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (4.0.10)\r\n", "Requirement already satisfied: jupyterlab-widgets~=3.0.10 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (3.0.10)\r\n", "Requirement already satisfied: decorator in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (5.1.1)\r\n", "Requirement already satisfied: jedi>=0.16 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.1)\r\n", "Requirement already satisfied: matplotlib-inline in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.6)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.43)\r\n", "Requirement already satisfied: pygments>=2.4.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (2.17.2)\r\n", "Requirement already satisfied: stack-data in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\r\n", "Requirement already satisfied: pexpect>4.3 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\r\n", "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.4)\r\n", "Requirement already satisfied: ptyprocess>=0.5 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\r\n", "Requirement already satisfied: wcwidth in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\r\n", "Requirement already satisfied: executing>=1.2.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.0.1)\r\n", "Requirement already satisfied: asttokens>=2.1.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.4.1)\r\n", "Requirement already satisfied: pure-eval in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (0.2.2)\r\n", "Requirement already satisfied: six>=1.12.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from asttokens>=2.1.0->stack-data->ipython>=6.1.0->ipywidgets) (1.16.0)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "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", "id": "90efc3d3-bf00-423d-ba81-246e4763b499", "metadata": {}, "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, "id": "d6afb8f9-3872-4a7e-a76d-0b50855fe089", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:40.859077Z", "iopub.status.busy": "2024-04-14T18:57:40.857904Z", "iopub.status.idle": "2024-04-14T18:57:46.230603Z", "shell.execute_reply": "2024-04-14T18:57:46.229895Z" } }, "outputs": [], "source": [ "import numpy as np\n", "\n", "import pybop" ] }, { "cell_type": "markdown", "id": "a976f817-f0b3-421e-8cd3-a49be9128068", "metadata": {}, "source": [ "## Importing Parameters\n", "\n", "This can be completed by importing a JSON representation, such as the one in the PyBOP [examples](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/parameters/initial_ecm_parameters.json). To import via JSON, either download the example file, or create your own and update the path below to reference the corresponding file." ] }, { "cell_type": "code", "execution_count": 3, "id": "734d6d86-61e3-4125-bcea-e83b3235814b", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.247269Z", "iopub.status.busy": "2024-04-14T18:57:46.246744Z", "iopub.status.idle": "2024-04-14T18:57:46.249217Z", "shell.execute_reply": "2024-04-14T18:57:46.248814Z" } }, "outputs": [], "source": [ "# parameter_set = pybop.ParameterSet(\n", "# json_path=\"examples/scripts/parameters/initial_ecm_parameters.json\"\n", "# )\n", "# parameter_set.import_parameters()" ] }, { "cell_type": "markdown", "id": "11f17daf-4a04-4ccd-8175-8e5a37f79f7f", "metadata": {}, "source": [ "Alternatively, define the initial parameter set with a dictionary. Ensure you have definitions for all R's, C's, and initial overpotentials for any additional RC elements.\n", "\n", "In this example, we use the default parameter value for the \"Open-circuit voltage [V] as provided by the original PyBaMM class. To update this, provide a function definition that matches this [function](https://github.com/pybamm-team/PyBaMM/blob/1943aa5ab2895b5378220595923dbae3d66b13c9/pybamm/input/parameters/ecm/example_set.py#L17)." ] }, { "cell_type": "code", "execution_count": 4, "id": "8d4a0635-51da-4998-8b48-deda13a49e39", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.251312Z", "iopub.status.busy": "2024-04-14T18:57:46.251183Z", "iopub.status.idle": "2024-04-14T18:57:46.424946Z", "shell.execute_reply": "2024-04-14T18:57:46.424250Z" } }, "outputs": [], "source": [ "parameter_set = pybop.ParameterSet(\n", " params_dict={\n", " \"chemistry\": \"ecm\",\n", " \"Initial SoC\": 0.5,\n", " \"Initial temperature [K]\": 25 + 273.15,\n", " \"Cell capacity [A.h]\": 5,\n", " \"Nominal cell capacity [A.h]\": 5,\n", " \"Ambient temperature [K]\": 25 + 273.15,\n", " \"Current function [A]\": 5,\n", " \"Upper voltage cut-off [V]\": 4.2,\n", " \"Lower voltage cut-off [V]\": 3.0,\n", " \"Cell thermal mass [J/K]\": 1000,\n", " \"Cell-jig heat transfer coefficient [W/K]\": 10,\n", " \"Jig thermal mass [J/K]\": 500,\n", " \"Jig-air heat transfer coefficient [W/K]\": 10,\n", " \"Open-circuit voltage [V]\": pybop.empirical.Thevenin().default_parameter_values[\n", " \"Open-circuit voltage [V]\"\n", " ],\n", " \"R0 [Ohm]\": 0.001,\n", " \"Element-1 initial overpotential [V]\": 0,\n", " \"Element-2 initial overpotential [V]\": 0,\n", " \"R1 [Ohm]\": 0.0002,\n", " \"R2 [Ohm]\": 0.0003,\n", " \"C1 [F]\": 13000,\n", " \"C2 [F]\": 36924,\n", " \"Entropic change [V/K]\": 0.0004,\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "017695fd-ee78-4113-af18-2fea04cf6126", "metadata": {}, "source": [ "## Identifying the Parameters\n", "\n", "Now that the initial parameter set is constructed, we can start the PyBOP fitting process. First, we define the model class with two RC elements." ] }, { "cell_type": "code", "execution_count": 5, "id": "e84b6dd0-8f9e-4b68-b7cb-f3bcb9988802", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.427992Z", "iopub.status.busy": "2024-04-14T18:57:46.427831Z", "iopub.status.idle": "2024-04-14T18:57:46.432028Z", "shell.execute_reply": "2024-04-14T18:57:46.431542Z" } }, "outputs": [], "source": [ "model = pybop.empirical.Thevenin(\n", " parameter_set=parameter_set, options={\"number of rc elements\": 2}\n", ")" ] }, { "cell_type": "markdown", "id": "bf63b4f9-de38-4e70-9472-1de4973a0954", "metadata": {}, "source": [ "In this example, we are going to try to fit all five parameters at once. This isn't recommend for real-life application as identifiablity is challenging to guarantee with this large a parameter space. To do this, we define the `pybop.parameters` as," ] }, { "cell_type": "code", "execution_count": 6, "id": "e75da7e3-8815-4159-a5ad-600a235b028c", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.434256Z", "iopub.status.busy": "2024-04-14T18:57:46.434089Z", "iopub.status.idle": "2024-04-14T18:57:46.436984Z", "shell.execute_reply": "2024-04-14T18:57:46.436537Z" } }, "outputs": [], "source": [ "parameters = [\n", " pybop.Parameter(\n", " \"R0 [Ohm]\",\n", " prior=pybop.Gaussian(0.0002, 0.0001),\n", " bounds=[1e-4, 1e-2],\n", " ),\n", " pybop.Parameter(\n", " \"R1 [Ohm]\",\n", " prior=pybop.Gaussian(0.0001, 0.0001),\n", " bounds=[1e-5, 1e-2],\n", " ),\n", " pybop.Parameter(\n", " \"R2 [Ohm]\",\n", " prior=pybop.Gaussian(0.0001, 0.0001),\n", " bounds=[1e-5, 1e-2],\n", " ),\n", " pybop.Parameter(\n", " \"C1 [F]\",\n", " prior=pybop.Gaussian(10000, 2500),\n", " bounds=[2500, 5e4],\n", " ),\n", " pybop.Parameter(\n", " \"C1 [F]\",\n", " prior=pybop.Gaussian(10000, 2500),\n", " bounds=[2500, 5e4],\n", " ),\n", "]" ] }, { "cell_type": "markdown", "id": "3ab5afb4-5007-4cef-9802-c25dc077e466", "metadata": {}, "source": [ "Let's create some synthetic data to identify the parameters. This data is then corrupted with a small amount of Gaussian noise to represent some additional uncertainty in the measured values. We can then form the `pybop.Dataset` from this data." ] }, { "cell_type": "code", "execution_count": 7, "id": "c346b106-99a9-46bc-8b5d-d330ed911660", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.438835Z", "iopub.status.busy": "2024-04-14T18:57:46.438684Z", "iopub.status.idle": "2024-04-14T18:57:46.478613Z", "shell.execute_reply": "2024-04-14T18:57:46.478339Z" } }, "outputs": [], "source": [ "sigma = 0.001\n", "t_eval = np.arange(0, 900, 2)\n", "values = model.predict(t_eval=t_eval)\n", "corrupt_values = values[\"Voltage [V]\"].data + np.random.normal(0, sigma, len(t_eval))\n", "\n", "# Form dataset\n", "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", "id": "8ce6c438-a402-4b1b-ad8a-598ceee74f2f", "metadata": {}, "source": [ "The `FittingProblem` class provides us with a single class that holds all of the objects we need to evaluate our selected `SumSquaredError` cost function. " ] }, { "cell_type": "code", "execution_count": 8, "id": "62369a4d-96e5-49d2-8951-4468b3fc5831", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.480234Z", "iopub.status.busy": "2024-04-14T18:57:46.480123Z", "iopub.status.idle": "2024-04-14T18:57:46.488949Z", "shell.execute_reply": "2024-04-14T18:57:46.488688Z" } }, "outputs": [], "source": [ "problem = pybop.FittingProblem(model, parameters, dataset)\n", "cost = pybop.SumSquaredError(problem)" ] }, { "cell_type": "markdown", "id": "ab62ee34-85ee-4b5a-ab25-3bd7dd47f312", "metadata": {}, "source": [ "The cost function can be interrogated manually via the `cost([params])` API. In this example, that would look like the following," ] }, { "cell_type": "code", "execution_count": 9, "id": "f69b34f5-0b46-4646-acbe-991046997b98", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.490406Z", "iopub.status.busy": "2024-04-14T18:57:46.490322Z", "iopub.status.idle": "2024-04-14T18:57:46.510798Z", "shell.execute_reply": "2024-04-14T18:57:46.510375Z" } }, "outputs": [ { "data": { "text/plain": [ "0.024944621550803514" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost([0.001, 0.001, 0.001, 5000, 5000])" ] }, { "cell_type": "markdown", "id": "3ef5b0da-f755-43c6-8904-79d7ee0f218c", "metadata": {}, "source": [ "Next, we construct the optimisation class with our algorithm of choice and run it. In this case, we select the CMA-ES method as it provides global optimisation capability. For the sake of reducing the runtime of this example, we limit the maximum iterations to 100; however, feel free to update this value." ] }, { "cell_type": "code", "execution_count": 10, "id": "6244882e-11ad-4bfe-a512-f1c687a06a08", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:46.512725Z", "iopub.status.busy": "2024-04-14T18:57:46.512597Z", "iopub.status.idle": "2024-04-14T18:57:49.259154Z", "shell.execute_reply": "2024-04-14T18:57:49.257712Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initial parameters: [2.29696565e-04 3.53341865e-05 1.63145688e-05 1.07259649e+04\n", " 9.73352990e+03]\n", "Estimated parameters: [1.00367942e-03 3.80907013e-04 1.00242178e-04 1.07259636e+04\n", " 9.73353073e+03]\n" ] } ], "source": [ "optim = pybop.Optimisation(cost, optimiser=pybop.CMAES)\n", "optim.set_max_iterations(300)\n", "x, final_cost = optim.run()\n", "print(\"Initial parameters:\", cost.x0)\n", "print(\"Estimated parameters:\", x)" ] }, { "cell_type": "markdown", "id": "93ee37a3-67f6-4c6a-a05d-507700cfa9da", "metadata": {}, "source": [ "## Plotting and Visualisation\n", "\n", "PyBOP provides various plotting utilities to visualize the results of the optimisation." ] }, { "cell_type": "code", "execution_count": 11, "id": "2cec5659-31fa-4164-82f0-4467a4894729", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:49.273422Z", "iopub.status.busy": "2024-04-14T18:57:49.272340Z", "iopub.status.idle": "2024-04-14T18:57:50.177989Z", "shell.execute_reply": "2024-04-14T18:57:50.173807Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "02004006008003.63.623.643.663.68ReferenceModelOptimised ComparisonTime / sVoltage / V" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pybop.quick_plot(problem, parameter_values=x, title=\"Optimised Comparison\");" ] }, { "cell_type": "markdown", "id": "7d265967-e6e5-440c-badf-156a43943c88", "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": 12, "id": "f66e0b0f-4861-42dd-bb7f-8734fcca3328", "metadata": { "execution": { "iopub.execute_input": "2024-04-14T18:57:50.189616Z", "iopub.status.busy": "2024-04-14T18:57:50.188971Z", "iopub.status.idle": "2024-04-14T18:57:52.898771Z", "shell.execute_reply": "2024-04-14T18:57:52.897811Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "51015202500.0020.0040.0060.0080.010.0120.0140.016ConvergenceIterationCost" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "0501001502000.00050.0010.001505010015020000.00050.0010501001502000200μ400μ600μ800μ05010015020010.725962k10.725963k10.725964k10.725965k0501001502009,733.5299,733.539,733.5319,733.5329,733.533R0 [Ohm]R1 [Ohm]R2 [Ohm]C1 [F]C1 [F]Parameter ConvergenceFunction CallFunction CallFunction CallFunction CallFunction CallR0 [Ohm]R1 [Ohm]R2 [Ohm]C1 [F]C1 [F]" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pybop.plot_convergence(optim)\n", "pybop.plot_parameters(optim);" ] }, { "cell_type": "markdown", "id": "c544a81c-1215-4794-b7db-c57c46125c77", "metadata": {}, "source": [ "### Conclusion\n", "\n", "This notebook illustrates how to perform parameter estimation using CMA-ES in PyBOP, providing insights into the optimisation process through various visualisations." ] } ], "metadata": { "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.12.2" } }, "nbformat": 4, "nbformat_minor": 5 }