{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# 3. Cycling data\n", "In this example, we show how to use full-cell cycling data to fit model parameters. Specifically, we will fit the negative electrode reference exchange-current density. The workflow shown in this example is very general and can take _any_ cycling data and fit the model to _any_ collected data. Typically we fit the voltage and/or temperature.\n", "\n", "Before running this example, make sure to run the script `true_parameters/generate_data.py` to generate the synthetic data." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "\n", "import ionworkspipeline as iwp\n", "import iwutil\n", "import pybamm\n", "\n", "from true_parameters.parameters import full_cell" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load the data\n", "\n", "This workflow can fit across multiple data at once. Here we will fit the time vs voltage data collected from three constant current experiments. We load the data into a dictionary whose keys are the C-rates." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data = {}\n", "for rate in [0.1, 0.5, 1]:\n", " data[f\"{rate} C\"] = iwutil.read_df(\n", " Path(\"synthetic_data\") / \"full_cell\" / f\"{str(rate).replace('.', '_')}C.csv\"\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set up the parameters to fit\n", "\n", "Now we set up our initial guesses and bounds for the parameters. As in the previous example, we create a dictionary of `Parameter` objects. We assume a symmetric Butler-Volmer exchange-current density. We create a function for the exchange-current density and fit a scalar reference exchange-current density.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def j0_n(c_e, c_s_surf, c_s_max, T):\n", " j0_ref = pybamm.Parameter(\n", " \"Negative electrode reference exchange-current density [A.m-2]\"\n", " )\n", " alpha = 0.5\n", " c_e_ref = 1000\n", "\n", " return (\n", " j0_ref\n", " * (c_e / c_e_ref) ** (1 - alpha)\n", " * (c_s_surf / c_s_max) ** alpha\n", " * (1 - c_s_surf / c_s_max) ** (1 - alpha)\n", " )\n", "\n", "\n", "parameters = {\n", " \"Negative electrode exchange-current density [A.m-2]\": j0_n,\n", " \"Negative electrode reference exchange-current density [A.m-2]\": iwp.Parameter(\n", " \"j0_n [A.m-2]\",\n", " initial_value=8,\n", " bounds=(0.1, 10),\n", " ),\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Get the known parameters\n", "\n", "In this example, we assume we already know all of the model parameters except for the negative electrode diffusivity and reference exchange-current density. We load the known parameters from the true parameters script." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "true_params = full_cell()\n", "known_params = {k: v for k, v in true_params.items() if k not in parameters}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Run the workflow\n", "\n", "Now we are ready to run the workflow. To run the workflow with the default settings, we can use the `fit_plot_save` function. This function runs the fit, saves the results (if a `save_dir` keyword argument is provided), and plots the results. All of the workflows have a common interface, where you typically need to provide the data, the parameters to be fitted, and a dictionary of known parameters.\n", "\n", "Here we use the \"current-driven\" workflow, which takes the input current from the data and fits the model output to the objective variables specified by the user. By default it fits the voltage. In the case where `data` is a dictionary of multiple data sets, the simultaneously optimizes across all data. \n", "\n", "As well as passing the data, parameters, and known parameters, we can pass additional keyword arguments to customize the fit. Here we pass a dictionary `objective_kwargs` to specify the model to use. Here we use the SPMe model. If no model is specified, an error is raised. We remove the events from the model to avoid triggering the voltage cut-off event.\n", "\n", "For this workflow, we also use a the `custom_parameters` keyword argument, which allows us to pass additional parameters to the objective function that are specific to each objective. Here we use the `custom_parameters` to set the initial concentration in each electrode to match the initial voltage in the data, assuming the cell is at equilibrium at the start of each experiment. We can use the built-in `initial_concentration_from_voltage` function to calculate the initial concentration in each electrode. This function takes the cell type (\"half\" or \"full\") and the electrode (\"negative\" or \"positive\") as inputs, and calculates the initial concentration based on the voltage using the function `pybamm.lithium_ion.get_initial_stoichiometries`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model = pybamm.lithium_ion.SPMe()\n", "model.events = []\n", "options = {\"model\": model}\n", "\n", "custom_parameters = {\n", " \"Initial concentration in negative electrode [mol.m-3]\": iwp.data_fits.custom_parameters.initial_concentration_from_voltage(\n", " \"full\", \"negative\"\n", " ),\n", " \"Initial concentration in positive electrode [mol.m-3]\": iwp.data_fits.custom_parameters.initial_concentration_from_voltage(\n", " \"full\", \"positive\"\n", " ),\n", "}\n", "objective_kwargs = {\"options\": options}\n", "res, figs = iwp.workflows.current_driven.fit_plot_save(\n", " data,\n", " parameters,\n", " known_params,\n", " objective_kwargs=objective_kwargs,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can take a look at the fitted parameters by accessing the `parameter_values` attribute of the `Results` object." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "res.parameter_values" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What's going on? A lower-level interface\n", "\n", "The workflows are designed to be simple to use and cover most use cases. They can be customized by passing additional keyword arguments. For example, we can pass a dictionary `datafit_kwargs` to specify the cost function or optimizer to be used in the fit. Much more customization is possible by accessing the underlying `DataFit` and `Optimizer` classes directly. Here we will go through the steps of running the fit manually.\n", "\n", "We can use the same `parameters` and `known_params` as dicitonaries as before, but we need to create the `Objective` and `DataFit` objects manually.\n", "\n", "Let's start by creating the objective function(s). We use the `CurrentDriven` objective function and pass in the data and model. Since we are fitting multiple data sets, we create a corresponding dictionary of `Objective` objects. We can use the same `options` and `custom_parameters` as before.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "objectives = {}\n", "for this_name, this_data in data.items():\n", " objectives[this_name] = iwp.objectives.CurrentDriven(\n", " this_data, options=options, custom_parameters=custom_parameters\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next we create the `DataFit` object. Here we can also specify the cost function and optimizer to be used in the fit." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cost = iwp.costs.RMSE()\n", "optimizer = iwp.optimizers.ScipyMinimize(method=\"Nelder-Mead\")\n", "datafit = iwp.DataFit(objectives, parameters=parameters, cost=cost, optimizer=optimizer)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we run the fit and plot the results." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "results = datafit.run(known_params)\n", "_ = datafit.plot_trace()\n", "_ = datafit.plot_fit_results()" ] } ], "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" } }, "nbformat": 4, "nbformat_minor": 2 }