{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Estimating ECM Parameters & Running a Thermal Submodel\n", "\n", "This notebook provides example usage for estimating stationary parameters for a two RC branch Thevenin model. With the estimated parameters, a thermal model is created and predictions are made.\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": [ "/home/engs2510/Documents/Git/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", "/home/engs2510/Documents/Git/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 openpyxl pandas -q\n", "%pip install pybop -q\n", "\n", "import pandas as pd\n", "import pybamm\n", "\n", "import pybop\n", "\n", "pybop.PlotlyManager().pio.renderers.default = \"notebook_connected\"" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "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). First, we load and update the ECM input parameters," ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": {}, "outputs": [], "source": [ "parameter_set = pybop.ParameterSet.pybamm(\"ECM_Example\")\n", "parameter_set.update(\n", " {\n", " \"Cell capacity [A.h]\": 3,\n", " \"Nominal cell capacity [A.h]\": 3,\n", " \"Element-1 initial overpotential [V]\": 0,\n", " \"Upper voltage cut-off [V]\": 4.2,\n", " \"Lower voltage cut-off [V]\": 2.5,\n", " \"R0 [Ohm]\": 1e-3,\n", " \"R1 [Ohm]\": 3e-3,\n", " \"C1 [F]\": 5e2,\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.002,\n", " \"C2 [F]\": 3e4,\n", " \"Element-2 initial overpotential [V]\": 0,\n", " },\n", " check_already_exists=False,\n", ")" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "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": "5", "metadata": {}, "outputs": [], "source": [ "model = pybop.empirical.Thevenin(\n", " parameter_set=parameter_set,\n", " options={\"number of rc elements\": 2},\n", " solver=pybamm.CasadiSolver(mode=\"safe\", dt_max=10),\n", ")" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "Next we need to select the data for parameter identification. In this example we use a single HPPC pulse from the \n", "`Kollmeyer, Phillip; Skells, Michael (2020), “Samsung INR21700 30T 3Ah Li-ion Battery Data”, Mendeley Data, V1, doi: 10.17632/9xyvy2njj3.1` dataset. This is imported and used to construct the `pybop.Dataset` class," ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [], "source": [ "file_loc = r\"../data/Samsung_INR21700/sample_hppc_pulse.xlsx\"\n", "df = pd.read_excel(file_loc, index_col=None, na_values=[\"NA\"])\n", "df = df.drop_duplicates(subset=[\"Time\"], keep=\"first\")\n", "\n", "dataset = pybop.Dataset(\n", " {\n", " \"Time [s]\": df[\"Time\"].to_numpy(),\n", " \"Current function [A]\": df[\"Current\"].to_numpy(),\n", " \"Voltage [V]\": df[\"Voltage\"].to_numpy(),\n", " }\n", ")" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "Next, we need to define the parameter for identification. In this example, we've construct a two-branch Thevenin model, so we will select those parameters for identification. The initial guess for the resistance parameter is generated from a random sample of the prior distributions. These are influenced by the `r_guess` parameter below." ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": {}, "outputs": [], "source": [ "r_guess = 0.005\n", "parameters = pybop.Parameters(\n", " pybop.Parameter(\n", " \"R0 [Ohm]\",\n", " prior=pybop.Gaussian(r_guess, r_guess / 10),\n", " bounds=[0, 0.2],\n", " ),\n", " pybop.Parameter(\n", " \"R1 [Ohm]\",\n", " prior=pybop.Gaussian(r_guess, r_guess / 10),\n", " bounds=[0, 0.2],\n", " ),\n", " pybop.Parameter(\n", " \"R2 [Ohm]\",\n", " prior=pybop.Gaussian(r_guess, r_guess / 10),\n", " bounds=[0, 0.2],\n", " ),\n", " pybop.Parameter(\n", " \"C1 [F]\",\n", " prior=pybop.Gaussian(500, 100),\n", " bounds=[100, 10000],\n", " ),\n", " pybop.Parameter(\n", " \"C2 [F]\",\n", " prior=pybop.Gaussian(2000, 500),\n", " bounds=[100, 10000],\n", " ),\n", ")" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "The `FittingProblem` class provides us with a single class that holds the objects we need to evaluate our selected `SumSquaredError` cost function. As we haven't built the model, we first do that with an initial OCV state selected from the first data point in the HPPC pulse." ] }, { "cell_type": "code", "execution_count": null, "id": "11", "metadata": {}, "outputs": [], "source": [ "model.build(\n", " initial_state={\"Initial open-circuit voltage [V]\": df[\"Voltage\"].to_numpy()[0]}\n", ")\n", "problem = pybop.FittingProblem(\n", " model,\n", " parameters,\n", " dataset,\n", ")\n", "\n", "cost = pybop.SumSquaredError(problem)" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "Next, we construct the optimisation class with our algorithm of choice and run it. In this case, we select the XNES 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. Due to the scale differences in the parameters, we update the optimiser step-size (`sigma0`) to be parameter specific, which helps ensure the optimiser explores the complete parameter space." ] }, { "cell_type": "code", "execution_count": null, "id": "13", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initial parameters: [4.81653818e-03 4.49143448e-03 6.17816943e-03 6.71210862e+02\n", " 1.30328804e+03]\n", "Estimated parameters: [9.63365968e-03 3.70346684e-03 2.03665595e-02 2.79021132e+02\n", " 1.49068535e+03]\n" ] } ], "source": [ "optim = pybop.XNES(\n", " cost,\n", " sigma0=[1e-3, 1e-3, 1e-3, 20, 20],\n", " max_unchanged_iterations=30,\n", " max_iterations=100,\n", ")\n", "x, final_cost = optim.run()\n", "print(\"Initial parameters:\", optim.x0)\n", "print(\"Estimated parameters:\", x)" ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "## Plotting and Visualisation\n", "\n", "Next, we use PyBOP's plotting utilities to visualise the results of the optimisation. This provides us with a visual confirmation of the optimisers' converged parameter values in the time-domain output." ] }, { "cell_type": "code", "execution_count": null, "id": "15", "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "