{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Introduction to Transformations\n", "This example introduces the `pybop.BaseTransformation` class and its instances. This class enables the cost and likelihood functions to be evaluated in a transformed search space. The search space is used by the optimiser and sampler classes during inference. These transformations can be both linear (e.g. `pybop.ScaledTransformation`) and non-linear (e.g. `pybop.LogTransformation`). By default, if transformations are applied, the sampling and optimisers will search in the transformed space.\n", "\n", "Transformations 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": [], "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.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "First, to showcase the transformation functionality, we need to construct a cost. This class is typically built on 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 transformations." ] }, { "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 transformations applied to each parameter. PyBOP allows for transformations to be applied at the individual parameter level, which are then combined for application during the optimisation. Below we 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 translation or 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(\n", " coefficient=1 / 0.35, intercept=-0.35\n", " ),\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(\n", " coefficient=1 / 0.175, intercept=-0.45\n", " ),\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 and cost classes." ] }, { "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.006904000484441733" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost([0.6, 0.6])" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "The optimiser and sampler classes call the cost via the `pybop.CostInterface` which applies the transformations to the search parameters from the optimiser (using `to_model`) and then optionally returns the gradient with respect to the search parameters (using the `jacobian`). We can check that the transformation is applied during optimisation by creating an optimisation class." ] }, { "cell_type": "code", "execution_count": null, "id": "13", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.006904000484441733" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "optim = pybop.Optimisation(cost=cost)\n", "x0 = parameters.initial_value(apply_transform=True)\n", "optim.call_cost(x0, cost=cost)" ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "We can see that the result of the two cost evaluations (with and without transformations) by comparing the output." ] }, { "cell_type": "code", "execution_count": null, "id": "15", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "optim.call_cost(parameters.initial_value(apply_transform=True), cost=cost) == cost(\n", " parameters.initial_value()\n", ")" ] }, { "cell_type": "markdown", "id": "16", "metadata": {}, "source": [ "We can also compare the cost landscapes plotted in the model and search spaces. Let's first plot the cost in the model space through the conventional method:" ] }, { "cell_type": "code", "execution_count": null, "id": "17", "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "