{ "cells": [ { "cell_type": "markdown", "id": "0", "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": null, "id": "1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/Users/engs2510/Documents/Git/Second_PyBOP/.nox/notebooks-overwrite/bin/python3: No module named pip\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": [ "/Users/engs2510/Documents/Git/Second_PyBOP/.nox/notebooks-overwrite/bin/python3: No module named pip\r\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 -q\n", "%pip install pybop -q" ] }, { "cell_type": "markdown", "id": "2", "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": null, "id": "3", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pybamm\n", "\n", "import pybop\n", "\n", "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "Let's fix the random seed in order to generate consistent output during development, although this does not need to be done in practice." ] }, { "cell_type": "code", "execution_count": null, "id": "5", "metadata": {}, "outputs": [], "source": [ "np.random.seed(8)" ] }, { "cell_type": "markdown", "id": "6", "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": null, "id": "7", "metadata": {}, "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": "8", "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": null, "id": "9", "metadata": {}, "outputs": [], "source": [ "parameter_set = pybamm.ParameterValues(\"ECM_Example\")\n", "parameter_set.update(\n", " {\n", " \"Cell capacity [A.h]\": 5,\n", " \"Nominal cell capacity [A.h]\": 5,\n", " \"Current function [A]\": 5,\n", " \"Initial SoC\": 0.5,\n", " \"Element-1 initial overpotential [V]\": 0,\n", " \"Upper voltage cut-off [V]\": 4.2,\n", " \"Lower voltage cut-off [V]\": 3.0,\n", " \"R0 [Ohm]\": 1e-3,\n", " \"R1 [Ohm]\": 2e-4,\n", " \"C1 [F]\": 1e4,\n", " \"Open-circuit voltage [V]\": pybop.empirical.Thevenin().default_parameter_values[\n", " \"Open-circuit voltage [V]\"\n", " ],\n", " }\n", ")\n", "# Optional arguments - only needed for two RC pairs\n", "parameter_set.update(\n", " {\n", " \"R2 [Ohm]\": 0.0003,\n", " \"C2 [F]\": 40000,\n", " \"Element-2 initial overpotential [V]\": 0,\n", " },\n", " check_already_exists=False,\n", ")" ] }, { "cell_type": "markdown", "id": "10", "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": null, "id": "11", "metadata": {}, "outputs": [], "source": [ "model = pybop.empirical.Thevenin(\n", " parameter_set=parameter_set, options={\"number of rc elements\": 2}\n", ")" ] }, { "cell_type": "markdown", "id": "12", "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": null, "id": "13", "metadata": {}, "outputs": [], "source": [ "parameters = pybop.Parameters(\n", " pybop.Parameter(\n", " \"R0 [Ohm]\",\n", " prior=pybop.Gaussian(2 - 4, 1e-4),\n", " bounds=[1e-4, 1e-2],\n", " ),\n", " pybop.Parameter(\n", " \"R1 [Ohm]\",\n", " prior=pybop.Gaussian(1e-4, 1e-4),\n", " bounds=[1e-5, 1e-2],\n", " ),\n", " pybop.Parameter(\n", " \"R2 [Ohm]\",\n", " prior=pybop.Gaussian(1e-4, 1e-4),\n", " bounds=[1e-5, 1e-2],\n", " ),\n", " pybop.Parameter(\n", " \"C1 [F]\",\n", " prior=pybop.Gaussian(1e4, 5e2),\n", " bounds=[2.5e3, 5e4],\n", " ),\n", " pybop.Parameter(\n", " \"C2 [F]\",\n", " prior=pybop.Gaussian(1e4, 5e2),\n", " bounds=[2.5e3, 5e4],\n", " ),\n", ")" ] }, { "cell_type": "markdown", "id": "14", "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": null, "id": "15", "metadata": {}, "outputs": [], "source": [ "sigma = 0.001\n", "t_eval = np.arange(0, 900, 3)\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": "16", "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": null, "id": "17", "metadata": {}, "outputs": [], "source": [ "problem = pybop.FittingProblem(model, parameters, dataset)\n", "cost = pybop.SumSquaredError(problem)" ] }, { "cell_type": "markdown", "id": "18", "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": null, "id": "19", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.017324210933474568" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost([0.001, 0.001, 0.001, 5000, 5000])" ] }, { "cell_type": "markdown", "id": "20", "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": null, "id": "21", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Halt: No significant change for 50 iterations.\n", "OptimisationResult:\n", " Initial parameters: [1.00990000e-04 2.09128273e-04 1.09990000e-05 9.30682523e+03\n", " 8.85175421e+03]\n", " Optimised parameters: [7.15236427e-04 4.90629050e-04 2.80089449e-04 9.29462538e+03\n", " 8.75074954e+03]\n", " Final cost: 0.0002759966220781992\n", " Optimisation time: 6.4131858348846436 seconds\n", " Number of iterations: 59\n", " SciPy result available: No\n" ] } ], "source": [ "optim = pybop.XNES(\n", " cost,\n", " sigma0=[1e-4, 1e-4, 1e-4, 10, 10],\n", " max_unchanged_iterations=50,\n", " max_iterations=250,\n", ")\n", "results = optim.run()" ] }, { "cell_type": "markdown", "id": "22", "metadata": {}, "source": [ "## Plotting and Visualisation\n", "\n", "PyBOP provides various plotting utilities to visualize the results of the optimisation." ] }, { "cell_type": "code", "execution_count": null, "id": "23", "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "