From cc73a4bf394f0c5217fc6cbeac8f63c9532073a0 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Thu, 22 Aug 2024 11:39:51 +0100 Subject: [PATCH 1/2] examples: adds example for cost/likelihood class evaluation and compute --- examples/notebooks/cost-compute-methods.ipynb | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 examples/notebooks/cost-compute-methods.ipynb diff --git a/examples/notebooks/cost-compute-methods.ipynb b/examples/notebooks/cost-compute-methods.ipynb new file mode 100644 index 000000000..8e2b87079 --- /dev/null +++ b/examples/notebooks/cost-compute-methods.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Using the Cost/Likelihood classes\n", + "This example will introduce the cost function methods used for both evaluating the output of and predicting the forward model. This example will use a cost class (pybop.SumofPower) as an example, but the methods discusse here are transferable to the other cost classes as well as the likelihood classes.\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 and upgrade dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n", + "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", + "\n", + "import pybop" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "To construct a `pybop.Cost` class, we need the following items first:\n", + "- Model\n", + "- Dataset\n", + "- Parameters to identify\n", + "- Problem\n", + "\n", + "Gien the above, we will first construct the model, then the parameters and corresponding dataset. Once that is complete, the problem will be created. Once we get to the cost, we will showcase the different interactions users can have with the cost class. A small example with evaluation as well as computation is presented." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "parameter_set = pybop.ParameterSet.pybamm(\"Chen2020\")\n", + "model = pybop.lithium_ion.SPM(parameter_set=parameter_set)" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "Now that we have the model constructed, let's create the problem class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "parameters = pybop.Parameters(\n", + " pybop.Parameter(\n", + " \"Negative electrode active material volume fraction\",\n", + " initial_value=0.6,\n", + " ),\n", + " pybop.Parameter(\n", + " \"Positive electrode active material volume fraction\",\n", + " initial_value=0.6,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "t_eval = np.linspace(0, 10, 100)\n", + "values = model.predict(t_eval=t_eval)\n", + "\n", + "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": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "problem = pybop.FittingProblem(model, parameters, dataset)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Perfect, let's not construct the cost class and move onto the main point of this example," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "cost = pybop.SumofPower(problem)" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "The conventional way to use the cost class is through the `cost.__call__` method, which is completed below," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.08963993888559865" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost([0.5, 0.5])" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "This does two things, it first evaluates the forward model at the given parameter values of `[0.5,0.5]`, then it computes the cost for the forward models prediction compared to the problem target values, which are provided from the dataset we constructed above. \n", + "\n", + "However, there is an alternative method to achieve this which provides the user with more flexibility in their assesment of the cost function, this is done through the `cost.compute` method, as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.08963993888559865" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "out = problem.evaluate([0.5, 0.5])\n", + "cost.compute(out)" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "This split the evaluation of the forward model and the computation of the cost function into two separate calls, allowing for the model evaluation to be decoupled from the cost computation. This decoupling can be helpful in the case where you want to assess the problem across multiple costs (see pybop.WeightedCost for a PyBOP implemenatio of this), or want to modify the problem output before assessing a cost.\n", + "\n", + "Next, let's present a few of these use-cases. In the first use-case, the problem is evaluated once, with random noise added and the cost computed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "def my_cost(inputs):\n", + " y = problem.evaluate(inputs)\n", + " y[\"Voltage [V]\"] += np.random.normal(0, 0.003, len(t_eval))\n", + " return cost.compute(y)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.08910088339381227" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_cost([0.5, 0.5])" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "The above method showcases how the `cost.__call__` method can be constructed at the user level. Furthermore, the above example can be reimplemented with gradient calculations as well via the `calculate_gradient` argument within the `cost.compute` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "def my_cost_gradient(inputs):\n", + " y, dy = problem.evaluateS1(inputs)\n", + " y[\"Voltage [V]\"] += np.random.normal(0, 0.003, len(t_eval))\n", + " return cost.compute(y, dy=dy, calculate_grad=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.08917807157201464, array([-0.57688969, -0.48453944]))" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_cost_gradient([0.5, 0.5])" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "The above method provides the computed cost for the parameter values, alongside the gradient with respect to those parameters. This is the exact structure that is used within PyBOP's gradient-based optimisers. Finally, the above can be easily reproduced via the `cost.__call__` method with the corresponding `calculate_gradient=True` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.08963668887423992, array([-0.58045629, -0.48653053]))" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost([0.5, 0.5], calculate_grad=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 83d5f09fc89c37e19f06103660ff270c9a5a3bb7 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 28 Aug 2024 09:09:18 +0100 Subject: [PATCH 2/2] examples: updates formatting and grammar --- examples/notebooks/cost-compute-methods.ipynb | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/examples/notebooks/cost-compute-methods.ipynb b/examples/notebooks/cost-compute-methods.ipynb index 8e2b87079..fc4947416 100644 --- a/examples/notebooks/cost-compute-methods.ipynb +++ b/examples/notebooks/cost-compute-methods.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "source": [ "# Using the Cost/Likelihood classes\n", - "This example will introduce the cost function methods used for both evaluating the output of and predicting the forward model. This example will use a cost class (pybop.SumofPower) as an example, but the methods discusse here are transferable to the other cost classes as well as the likelihood classes.\n", + "This example will introduce the cost function methods used for both evaluating the output of and predicting the forward model. This example will use a cost class (`pybop.SumofPower`) as an example, but the methods discussed here are transferable to the other cost classes as well as the likelihood classes.\n", "\n", "### Setting up the Environment\n", "\n", @@ -60,13 +60,13 @@ "id": "4", "metadata": {}, "source": [ - "To construct a `pybop.Cost` class, we need the following items first:\n", + "First, to construct a `pybop.Cost` class, we need the following objects:\n", "- Model\n", "- Dataset\n", "- Parameters to identify\n", "- Problem\n", "\n", - "Gien the above, we will first construct the model, then the parameters and corresponding dataset. Once that is complete, the problem will be created. Once we get to the cost, we will showcase the different interactions users can have with the cost class. A small example with evaluation as well as computation is presented." + "Given the above, we will first construct the model, then the parameters and corresponding dataset. Once that is complete, the problem will be created. With the cost class created, we will showcase the different interactions users can have with the class. A small example with evaluation as well as computation is presented." ] }, { @@ -85,7 +85,7 @@ "id": "6", "metadata": {}, "source": [ - "Now that we have the model constructed, let's create the problem class." + "Now that we have the model constructed, let's define the parameters for identification." ] }, { @@ -107,10 +107,18 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "Next, we generate some synthetic data from the model using the `model.predict` method. This then gets corrupted with Gaussian noise and used to create the Dataset." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -126,10 +134,18 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Now that we have the model, parameters, and dataset, we can combine them and construct the problem class. This class forms the basis for evaluating the forward model for the defined fitting process (parameters and operating conditions)." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -138,16 +154,16 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ - "Perfect, let's not construct the cost class and move onto the main point of this example," + "Perfect, let's now construct the cost class and move onto the main point of this example," ] }, { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -156,7 +172,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "The conventional way to use the cost class is through the `cost.__call__` method, which is completed below," @@ -165,7 +181,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [ { @@ -185,18 +201,18 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "This does two things, it first evaluates the forward model at the given parameter values of `[0.5,0.5]`, then it computes the cost for the forward models prediction compared to the problem target values, which are provided from the dataset we constructed above. \n", "\n", - "However, there is an alternative method to achieve this which provides the user with more flexibility in their assesment of the cost function, this is done through the `cost.compute` method, as shown below." + "However, there is an alternative method to achieve this which provides the user with more flexibility in their assessment of the cost function, this is done through the `cost.compute` method, as shown below." ] }, { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [ { @@ -217,10 +233,10 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ - "This split the evaluation of the forward model and the computation of the cost function into two separate calls, allowing for the model evaluation to be decoupled from the cost computation. This decoupling can be helpful in the case where you want to assess the problem across multiple costs (see pybop.WeightedCost for a PyBOP implemenatio of this), or want to modify the problem output before assessing a cost.\n", + "This splits the evaluation of the forward model and the computation of the cost function into two separate calls, allowing for the model evaluation to be decoupled from the cost computation. This decoupling can be helpful in the case where you want to assess the problem across multiple costs (see pybop.WeightedCost for a PyBOP implementation of this), or want to modify the problem output before assessing a cost.\n", "\n", "Next, let's present a few of these use-cases. In the first use-case, the problem is evaluated once, with random noise added and the cost computed." ] @@ -228,7 +244,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -241,7 +257,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "20", "metadata": {}, "outputs": [ { @@ -261,7 +277,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "21", "metadata": {}, "source": [ "The above method showcases how the `cost.__call__` method can be constructed at the user level. Furthermore, the above example can be reimplemented with gradient calculations as well via the `calculate_gradient` argument within the `cost.compute` method." @@ -270,7 +286,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -283,7 +299,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "23", "metadata": {}, "outputs": [ { @@ -303,16 +319,16 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "24", "metadata": {}, "source": [ - "The above method provides the computed cost for the parameter values, alongside the gradient with respect to those parameters. This is the exact structure that is used within PyBOP's gradient-based optimisers. Finally, the above can be easily reproduced via the `cost.__call__` method with the corresponding `calculate_gradient=True` argument." + "This provides the computed cost for the parameter values, alongside the gradient with respect to those parameters. This is the exact structure that is used within PyBOP's gradient-based optimisers. Finally, the above can be easily reproduced via the `cost.__call__` method with the corresponding `calculate_gradient=True` argument." ] }, { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "25", "metadata": {}, "outputs": [ {