{ "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": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pybop.quick_plot(problem, problem_inputs=x, title=\"Optimised Comparison\");" ] }, { "cell_type": "markdown", "id": "16", "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": null, "id": "17", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pybop.plot_convergence(optim)\n", "pybop.plot_parameters(optim);" ] }, { "cell_type": "markdown", "id": "18", "metadata": {}, "source": [ "## Using the estimated parameter for thermal predictions\n", "With the estimated RC parameters, the temperature distribution for a given drive cycle can be calculated using the identified Thevenin model. We now we use a `.xlsx` file containing time-series current data as a pybamm experiment. A sample file is used here, but user's may choose to upload customized drive cycle." ] }, { "cell_type": "code", "execution_count": null, "id": "19", "metadata": {}, "outputs": [], "source": [ "file_loc = r\"../data/Samsung_INR21700/sample_drive_cycle.xlsx\"\n", "df = pd.read_excel(file_loc, sheet_name=\"Sheet3\", index_col=None, na_values=[\"NA\"])\n", "\n", "# Remove duplicate rows, keeping the first occurrence\n", "df = df.drop_duplicates(subset=[\"Time\"], keep=\"first\")\n", "\n", "# Create the pybamm experiment\n", "experiment = pybamm.Experiment([pybamm.step.current(df.to_numpy())])" ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Step([[ 0 3]\n", " [ 1 3]\n", " [ 2 3]\n", " ...\n", " [3598 3]\n", " [3599 3]\n", " [3600 3]], duration=3600, period=1, direction=Discharge)]" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "experiment.steps" ] }, { "cell_type": "markdown", "id": "21", "metadata": {}, "source": [ "Update the estimated RC values. These values will be used to calculate heat generation and corresponding temperature distribution in the thermal submodel. Given `model.predict` is a light wrapper on the `PyBaMM.Simulation` class, we interact with it the same way. Visualisation of voltage response and cell temperature is plotted below using the PyBaMM solution." ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2024-09-17 13:35:23.641 - [WARNING] callbacks.on_experiment_infeasible_time(240): \n", "\n", "\tExperiment is infeasible: default duration (3600 seconds) was reached during 'Step([[ 0 3]\n", " [ 1 3]\n", " [ 2 3]\n", " ...\n", " [3598 3]\n", " [3599 3]\n", " [3600 3]], duration=3600, period=1, direction=Discharge)'. The returned solution only contains up to step 1 of cycle 1. Please specify a duration in the step instructions.\n" ] } ], "source": [ "sol = model.predict(\n", " inputs=x,\n", " experiment=experiment,\n", " parameter_set=parameter_set,\n", " initial_state={\"Initial SoC\": 0.95},\n", ")" ] }, { "cell_type": "markdown", "id": "23", "metadata": {}, "source": [ "## Conclusion\n", "\n", "This notebook illustrates how to extract EC parameters from an HPPC pulse using XNES in PyBOP, providing insights into the optimisation process through various visualisations. The estimated parameters are then used to run a thermal submodel." ] } ], "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 }