{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Investigating different cost functions\n", "\n", "In this notebook, we take a look at the different fitting cost functions offered in PyBOP. Cost functions for fitting problems conventionally describe the distance between two points (the target and the prediction) which is to be minimised via PyBOP's optimisation algorithms. \n", "\n", "First, we install and import the required packages below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%pip install --upgrade pip ipywidgets -q\n", "%pip install pybop -q\n", "\n", "import numpy as np\n", "\n", "import pybop\n", "\n", "go = pybop.PlotlyManager().go\n", "pybop.PlotlyManager().pio.renderers.default = \"notebook_connected\"" ] }, { "cell_type": "markdown", "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, "metadata": {}, "outputs": [], "source": [ "np.random.seed(8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For this notebook, we need to construct parameters, a model and a problem class before we can compare differing cost functions. We start with two parameters, but this is an arbitrary selection and can be expanded given the model and data in question." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "parameters = pybop.Parameters(\n", " pybop.Parameter(\n", " \"Positive electrode thickness [m]\",\n", " prior=pybop.Gaussian(7.56e-05, 0.5e-05),\n", " bounds=[65e-06, 10e-05],\n", " ),\n", " pybop.Parameter(\n", " \"Positive particle radius [m]\",\n", " prior=pybop.Gaussian(5.22e-06, 0.5e-06),\n", " bounds=[2e-06, 9e-06],\n", " ),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we will construct the Single Particle Model (SPM) with the Chen2020 parameter set, but like the above, this is an arbitrary selection and can be replaced with any PyBOP model." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "parameter_set = pybop.ParameterSet.pybamm(\"Chen2020\")\n", "model = pybop.lithium_ion.SPM(parameter_set=parameter_set)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, as we will need reference data to compare our model predictions to (via the cost function), we will create synthetic data from the model constructed above. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "t_eval = np.arange(0, 900, 10)\n", "values = model.predict(t_eval=t_eval)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can then construct the PyBOP dataset class with the synthetic data as," ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dataset = pybop.Dataset(\n", " {\n", " \"Time [s]\": t_eval,\n", " \"Current function [A]\": values[\"Current [A]\"].data,\n", " \"Voltage [V]\": values[\"Voltage [V]\"].data,\n", " }\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we can put this all together and construct the problem class. In this situation, we are going to compare differing fitting cost functions, so we construct the `FittingProblem`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "problem = pybop.FittingProblem(model, parameters, dataset)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Sum of Squared Errors and Root Mean Squared Error\n", "\n", "First, let's start with two commonly-used cost functions: the sum of squared errors (SSE) and the root mean squared error (RMSE). Constructing these classes is very concise in PyBOP, and only requires the problem class." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cost_SSE = pybop.SumSquaredError(problem)\n", "cost_RMSE = pybop.RootMeanSquaredError(problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we can investigate how these functions differ when fitting the parameters. To acquire the cost value for each of these, we can simply use the call method of the constructed class, such as:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1.1753460077019054e-09" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost_SSE([7.56e-05, 5.22e-06])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alternatively, we can use the `Parameters` class for this," ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[7.56e-05 5.22e-06]\n" ] }, { "data": { "text/plain": [ "1.1753460077019054e-09" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print(parameters.current_value())\n", "cost_SSE(parameters.current_value())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we want to generate a random sample of candidate solutions from the parameter class prior, we can also do that as:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[7.60957550e-05 5.48691392e-06]\n" ] }, { "data": { "text/plain": [ "0.014466627355628724" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sample = parameters.rvs()\n", "print(sample)\n", "cost_SSE(sample)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Comparing RMSE and SSE\n", "\n", "Now, let's vary one of the parameters, and keep a fixed value for the other, to create a scatter plot comparing the cost values for the RMSE and SSE functions." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "y_minkowski = []\n", "for i in x_range:\n", " y_minkowski.append(cost_minkowski([7.56e-05, i]))\n", "\n", "fig = go.Figure()\n", "fig.add_trace(\n", " go.Scatter(\n", " x=x_range,\n", " y=np.asarray(y_RMSE) * np.sqrt(len(t_eval)),\n", " mode=\"lines\",\n", " name=\"RMSE*N\",\n", " )\n", ")\n", "fig.add_trace(\n", " go.Scatter(\n", " x=x_range,\n", " y=np.sqrt(y_SSE),\n", " mode=\"lines\",\n", " line=dict(dash=\"dash\"),\n", " name=\"sqrt(SSE)\",\n", " )\n", ")\n", "fig.add_trace(\n", " go.Scatter(\n", " x=x_range, y=y_minkowski, mode=\"lines\", line=dict(dash=\"dot\"), name=\"Minkowski\"\n", " )\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, these lines lie on top of one another. Now, let's take a look at how the Minkowski cost changes for different orders, `p`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "p_orders = np.append(0.75, np.linspace(1, 3, 5))\n", "y_minkowski = tuple(\n", " [pybop.Minkowski(problem, p=j)([7.56e-05, i]) for i in x_range] for j in p_orders\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ "