{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# 3. Half-cell GITT" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we fit the exchange current density and diffusivity for the half-cells using synthetic GITT data. We use the half-cell MSMR functions fitted and saved in half-cell OCP notebook to create interpolants for the OCP as a function of stoichiometry.\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": 1, "metadata": { "ExecuteTime": { "end_time": "2024-03-28T15:31:33.631255Z", "start_time": "2024-03-28T15:31:32.572474Z" } }, "outputs": [], "source": [ "from pathlib import Path\n", "import ionworkspipeline as iwp\n", "import pandas as pd\n", "import json\n", "import pybamm\n", "import iwutil\n", "\n", "from plots import constant_current, drive_cycle\n", "from true_parameters.parameters import half_cell" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2024-03-28T15:31:33.633761Z", "start_time": "2024-03-28T15:31:33.632230Z" } }, "outputs": [], "source": [ "iwp.set_logging_level(\"INFO\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "We begin by making a list of \"known parameters\". These are values that are already known from other sources (e.g. spec sheet, direct measurement, other experiments). For this example we load the known parameters from the true parameters used to generate the synthetic data." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2024-03-28T15:31:33.637048Z", "start_time": "2024-03-28T15:31:33.634649Z" } }, "outputs": [], "source": [ "# list of parameters that are known (in practice these would be collected from a data\n", "# sheet or from additional experimental measurements)\n", "KNOWN_PARAMETERS = [\n", " # Separator\n", " \"Separator thickness [m]\",\n", " \"Separator porosity\",\n", " # Positive electrode\n", " \"Positive electrode thickness [m]\",\n", " \"Positive electrode conductivity [S.m-1]\",\n", " \"Positive electrode porosity\",\n", " \"Positive electrode active material volume fraction\",\n", " \"Positive particle radius [m]\",\n", " \"Positive electrode OCP entropic change [V.K-1]\",\n", " # Cell\n", " \"Electrode height [m]\",\n", " \"Electrode width [m]\",\n", " # these are user-specified at the cell level and may differ from the actual\n", " # voltage and capacity as measured in the experiment\n", " \"Lower voltage cut-off [V]\",\n", " \"Upper voltage cut-off [V]\",\n", " \"Open-circuit voltage at 0% SOC [V]\",\n", " \"Open-circuit voltage at 100% SOC [V]\",\n", " \"Nominal cell capacity [A.h]\",\n", " \"Current function [A]\",\n", "]" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Next, we create a function to fit the exchange current density and diffusivity for the half-cells using the synthetic GITT data for each electrode. \n", "\n", "The function begins by loading in the known parameter values and previously fitted parameters. It then loads in the data and sets up the objective function for the GITT experiment. Next we set up the objective function for the GITT experiment. In order to do this, we first split the data into cycles, then for each pulse we set up a `Pulse` objective, which takes in the data and some extra options, including the model to use for the fitting. We set this up using the `get_pulse_objectives_by_cycle` function. This returns a dictionary of `PulseHalfCell` objectives with keys corresponding to the cycle number. We pass the dictionary to the `DataFit` object, which will fit the parameters simultaneously across all objectives in the dictionary.\n", "\n", "We then set up the `iwp.Parameter` objects and functional form for any functions -- here we use a symmetric exchange-current density and scalar diffusivity. \n", "\n", "Finally the calculations and fits are put together in a pipeline. The pipeline takes in the known and previously calculated parameters, constructs an interpolant for the OCP, performs electrode State of Health calculations to get the minimum and maximum stoichiometries, and fits the exchange-current density and diffusivity for each electrode. The results are saved to a file." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "ExecuteTime": { "end_time": "2024-03-28T15:31:33.643311Z", "start_time": "2024-03-28T15:31:33.638117Z" } }, "outputs": [], "source": [ "def fit_save(electrode, cycles_to_use=None):\n", " # Load true parameters from which we will read in the \"known\" values\n", " true_parameter_values = pybamm.ParameterValues(half_cell(electrode))\n", " known_parameter_values = {k: true_parameter_values[k] for k in KNOWN_PARAMETERS}\n", " known_values = iwp.direct_entries.DirectEntry(\n", " known_parameter_values, \"Measured or assumed values\"\n", " )\n", "\n", " # Read in previously fitted OCP parameters\n", " # Note: PyBaMM expects the working electrode to be positive\n", " ocp = iwp.calculations.ocp_data_interpolant_from_csv(\n", " \"positive\",\n", " f\"fitted_parameters/{electrode}_electrode_ocp.csv\",\n", " )\n", " with open(f\"fitted_parameters/{electrode}_electrode_ocp.json\") as f:\n", " ocp_params = json.load(f)\n", " if electrode == \"negative\":\n", " ocp_params = iwp.data_fits.util.negative_to_positive_half_cell(ocp_params)\n", " Q_total = ocp_params[\"Half-cell positive electrode capacity [A.h]\"]\n", " ocp_params.update(\n", " {\n", " \"Positive electrode capacity [A.h]\": Q_total,\n", " }\n", " )\n", " ocp_params = iwp.direct_entries.DirectEntry(ocp_params, \"Fitted OCP parameters\")\n", "\n", " # Load GITT data\n", " gitt_data = pd.read_csv(\n", " Path(\"synthetic_data\") / f\"half_cell_{electrode}_electrode\" / \"gitt.csv\"\n", " )\n", " if cycles_to_use is not None:\n", " gitt_data = gitt_data[gitt_data[\"Cycle number\"].isin(cycles_to_use)]\n", "\n", " # Set up parameters to fit\n", " def j0_half_cell(c_e, c_s_surf, c_s_max, T):\n", " j0_ref = pybamm.Parameter(\n", " \"Positive 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", " gitt_params = {\n", " \"Positive electrode exchange-current density [A.m-2]\": j0_half_cell,\n", " \"Positive electrode reference exchange-current density [A.m-2]\": iwp.Parameter(\n", " \"Positive electrode exchange-current density [A.m-2]\", bounds=(0.1, 100)\n", " ),\n", " \"Positive particle diffusivity [m2.s-1]\": iwp.Parameter(\n", " \"Positive particle diffusivity [m2.s-1]\",\n", " initial_value=1e-14,\n", " bounds=(1e-15, 1e-13),\n", " ),\n", " }\n", "\n", " # Make data fit - we construct objectives for each GITT pulse and fit across\n", " # all of them simultaneously\n", " model = pybamm.lithium_ion.SPMe({\"working electrode\": \"positive\"})\n", " solver = pybamm.IDAKLUSolver()\n", " objectives = iwp.objectives.pulse.get_pulse_objectives_by_cycle(\n", " gitt_data,\n", " options={\"model\": model, \"solver\": solver},\n", " )\n", " gitt = iwp.DataFit(\n", " objectives,\n", " parameters=gitt_params,\n", " )\n", "\n", " # Construct pipeline\n", " initial_soc = 1\n", " pipeline = iwp.Pipeline(\n", " {\n", " **iwp.pipelines.half_cell.defaults(),\n", " \"known values\": known_values,\n", " \"OCP interpolant\": ocp,\n", " \"OCP parameters\": ocp_params,\n", " \"maximum concentration\": iwp.calculations.ElectrodeCapacity(\n", " \"positive\",\n", " unknown=\"maximum concentration\",\n", " method=\"capacity\",\n", " ),\n", " \"electrode SOH calculations\": iwp.calculations.ElectrodeSOHHalfCell(\n", " \"positive\"\n", " ),\n", " \"initial concentration\": iwp.calculations.InitialSOCHalfCell(\n", " \"positive\", initial_soc\n", " ),\n", " \"GITT\": gitt,\n", " },\n", " )\n", "\n", " # Run pipeline\n", " fitted_parameter_values = pipeline.run()\n", "\n", " # Save parameters to JSON\n", " params_to_save = {\n", " f\"{electrode.capitalize()} electrode reference exchange-current density [A.m-2]\": fitted_parameter_values[\n", " \"Positive electrode reference exchange-current density [A.m-2]\"\n", " ],\n", " f\"{electrode.capitalize()} particle diffusivity [m2.s-1]\": fitted_parameter_values[\n", " \"Positive particle diffusivity [m2.s-1]\"\n", " ],\n", " }\n", " iwutil.save.json(\n", " params_to_save,\n", " Path(\"fitted_parameters\") / f\"{electrode}_electrode_gitt.json\",\n", " )\n", "\n", " # Export a .py file with the PyBaMM parameters\n", " # Note: if we specify data_path=None, the variable DATA_PATH will be set to the\n", " # directory containing the exported script\n", " iwp.util.export_python_script(\n", " fitted_parameter_values,\n", " f\"fitted_parameters/{electrode}_half_cell.py\",\n", " data_path=None,\n", " )\n", " return pipeline, fitted_parameter_values" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "We run the fit for each electrode, storing the results in a dictionary. " ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "