Cycle-ageing fit

Fit an SEI solvent diffusivity from cycle-ageing summary data (loss of lithium inventory vs cycle number) with CycleAgeing and DataFit.

This fits LLI [%], a built-in default metric, so it needs no metrics option. A custom metric is expressible too, as a metrics config — this one is last-minus-first of the cycle-wise discharge capacity over each cycle’s first step:

metrics = {
    "C/5 capacity [A.h]": {
        "type": "ComposedMetric",
        "operation": "sub",
        "left": {
            "type": "CyclewiseMetric",
            "step": 0,
            "metric": {"type": "Last", "variable": "Discharge capacity [A.h]"},
        },
        "right": {
            "type": "CyclewiseMetric",
            "step": 0,
            "metric": {"type": "First", "variable": "Discharge capacity [A.h]"},
        },
    }
}

The summary data below has no discharge-capacity column to compare it against, so this fit omits it.

from ionworks import Ionworks
import ionworks_schema as iws
import matplotlib.pyplot as plt
import pandas as pd
import pybamm

# Synthetic example summary, one row per RPT: Cycle number and LLI [%]. Swap in your
# own export, or read a measurement with `client.cell_measurement`.
data = pd.read_csv(iws.example_data("cycle_ageing_summary"))

# A fit needs a complete parameter set, not just the ones you know.
known = iws.direct_entries.DirectEntry(
    parameters=dict(pybamm.ParameterValues("Chen2020"))
)

# RPT: a slow C/5 discharge (the LLI read) then a rest. `experiment` also
# takes UCP: https://docs.ionworks.com/simulate/universal-cycler-protocol
rpt = [
    {
        "type": "c-rate",
        "value": 0.2,
        "terminations": [{"type": "voltage", "value": 2.5}],
        "period": 60.0,
    },
    {"type": "rest", "duration": "10 minutes"},
]
# Faster cycle that ages the cell between RPTs. Both steps are constant
# current; add a voltage step for a CV taper.
cc_cycle = [
    {
        "type": "c-rate",
        "value": 1.0,
        "terminations": [{"type": "voltage", "value": 2.5}],
    },
    {
        "type": "c-rate",
        "value": -0.5,
        "terminations": [{"type": "voltage", "value": 4.2}],
    },
]
# 3 ageing cycles + 1 RPT per block; two blocks after an initial RPT gives
# the 9 cycles matching Cycle number 0, 4 and 8 in the data.
experiment = {"cycles": [rpt] + ([cc_cycle] * 3 + [rpt]) * 2}

objectives = {
    "aging": iws.objectives.CycleAgeing(
        data=data,
        options=iws.objectives.CycleAgeingOptions(
            model=pybamm.lithium_ion.SPMe(
                options={"SEI": "solvent-diffusion limited"}
            ),
            experiment=experiment,
            objective_variables=["LLI [%]"],
            # LLI [%] is a default metric, so `metrics` can stay unset.
        ),
    )
}
# We fit the SEI solvent diffusivity.
parameters = {
    "SEI solvent diffusivity [m2.s-1]": iws.Parameter(
        "SEI solvent diffusivity [m2.s-1]",
        initial_value=5e-19,
        bounds=(1e-20, 1e-18),
    ),
}

fit = iws.DataFit(
    objectives=objectives,
    parameters=parameters,
    cost=iws.costs.RMSE(),
    # Small population and iteration count keep the example quick; a
    # harder fit needs more.
    optimizer=iws.optimizers.PSO(population_size=10, max_iterations=15),
    # PSO is stochastic; the seed makes the run reproducible.
    options=iws.DataFitOptions(seed=0),
)

client = Ionworks()
submission = client.pipeline.create(iws.Pipeline({"known": known, "aging": fit}))
client.pipeline.wait_for_completion(submission.id)
result = client.pipeline.result(submission.id)
fit_result = result.element("aging")
figs = fit_result.plot_fit_results()
plt.show()
Model-vs-data overlay for the cycle-ageing fit