diff --git a/examples/notebooks/transformation_introduction.ipynb b/examples/notebooks/transformation_introduction.ipynb new file mode 100644 index 000000000..2c313fc6f --- /dev/null +++ b/examples/notebooks/transformation_introduction.ipynb @@ -0,0 +1,496 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Introduction to Transformations\n", + "This example introduces the `pybop.BaseTransformation` class and it's instances. This class adds functionality for the cost and likelihood functions to be transformed into a separate search space. This search space is used by the optimiser and sampler classes during inference. These transformations can be both linear (`pybop.ScaledTransformation`) and non-linear (`pybop.LogTransformation`). By default, if transformations are applied, the sampling and optimisers will search in the transformed space.\n", + "\n", + "Transformation can be helpful when the difference in parameter magnitudes is large, or to create a search space that is better posed for the optimisation algorithm. Before we begin, we need to ensure that we have all the necessary tools. We will install and import PyBOP alongside any other package 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\n", + "\n", + "import numpy as np\n", + "\n", + "import pybop\n", + "\n", + "pybop.PlotlyManager().pio.renderers.default = \"notebook_connected\"" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "First, to showcase the transformation functionality, we need to construct the `pybop.Cost` class. This class needs the following objects:\n", + "- Model\n", + "- Dataset\n", + "- Parameters to identify\n", + "- Problem\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "model = pybop.lithium_ion.SPM()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "Now that we have the model constructed, let's define the parameters for identification. At this point, we define the corresponding transformations applied to each parameter. PyBOP allows for transformations to be applied at the individual parameter level, which is then combined for application on each cost call. Below we will apply a linear transformation using the pybop `ScaledTransformation` class. This class has arguments for a `coefficient` which defines the linear stretch or scaling of the search space, and `intercept` which defines the space shift. The equation for this transformation is:\n", + "\n", + "$$\n", + "y_{search} = m*x_{model}+b\n", + "$$\n", + "\n", + "where $m$ is the linear scale coefficient, $b$ is the intercept, $x_{model}$ is the model parameter space, and $y_{search}$ is the transformed space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "parameters = pybop.Parameters(\n", + " pybop.Parameter(\n", + " \"Negative electrode active material volume fraction\",\n", + " initial_value=0.6,\n", + " bounds=[0.35, 0.7],\n", + " transformation=pybop.ScaledTransformation(coefficient=2.0, intercept=-0.6),\n", + " ),\n", + " pybop.Parameter(\n", + " \"Positive electrode active material volume fraction\",\n", + " initial_value=0.6,\n", + " bounds=[0.45, 0.625],\n", + " transformation=pybop.ScaledTransformation(coefficient=2.0, intercept=-0.6),\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "Next, to create the `pybop.Dataset` we generate some synthetic data from the model using the `model.predict` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "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": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "Now that we have the model, parameters, and dataset, we can combine them and construct the problem class. This is the last step needed before investigating how the transformation functionality works." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "problem = pybop.FittingProblem(model, parameters, dataset)\n", + "cost = pybop.SumofPower(problem)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "The conventional way to use the cost class is through the `cost.__call__` method, which is completed below without transformations applied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.006904000484442387" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost([0.6, 0.6])" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "However, we can also interact with the cost function with transformations applied via the `apply_transform` optional arugment. This arg is by default set to `False`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.006904000484442387" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost([0.0, 0.0], apply_transform=True)" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Given the transformations applied in the parameter class above, we can confirm the alignment between the search space and the model space by passing values that coincide:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost([0.0, 0.0], apply_transform=True) == cost([0.6, 0.6])" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "We can more thoroughly test the transformation by testing that the search space is scaled by a value of two through the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cost([0.05, 0.05], apply_transform=True) == cost([0.625, 0.625])" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Next, we can plot cost landscapes of these two spaces. In the first instance, we plot the model space through the conventional method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "