diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d258bff..6f905bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Features + +- [#316](https://github.com/pybop-team/PyBOP/pull/316) - Adds Adam with weight decay (AdamW) optimiser, adds depreciation warning for pints.Adam implementation. - [#271](https://github.com/pybop-team/PyBOP/issues/271) - Aligns the output of the optimisers via a generalisation of Result class. - [#315](https://github.com/pybop-team/PyBOP/pull/315) - Updates __init__ structure to remove circular import issues and minimises dependancy imports across codebase for faster PyBOP module import. Adds type-hints to BaseModel and refactors rebuild parameter variables. - [#236](https://github.com/pybop-team/PyBOP/issues/236) - Restructures the optimiser classes, adds a new optimisation API through direct construction and keyword arguments, and fixes the setting of `max_iterations`, and `_minimising`. Introduces `pybop.BaseOptimiser`, `pybop.BasePintsOptimiser`, and `pybop.BaseSciPyOptimiser` classes. diff --git a/README.md b/README.md index 6ff0af9f..8fd09c0a 100644 --- a/README.md +++ b/README.md @@ -85,13 +85,13 @@ The table below lists the currently supported [models](https://github.com/pybop- |-----------------------------------------------|-------------------------------------------------------------|------------------------------------------| | Single Particle Model (SPM) | Covariance Matrix Adaptation Evolution Strategy (CMA-ES) | Sum of Squared Errors (SSE) | | Single Particle Model with Electrolyte (SPMe) | Particle Swarm Optimization (PSO) | Root Mean Squared Error (RMSE) | -| Doyle-Fuller-Newman (DFN) | Adaptive Moment Estimation (Adam) | Maximum Likelihood Estimation (MLE) | -| Many Particle Model (MPM) | Improved Resilient Backpropagation (iRProp-) | Maximum a Posteriori (MAP) | -| Multi-Species Multi-Reactants (MSMR) | Exponential Natural Evolution Strategy (xNES) | Unscented Kalman Filter (UKF) | -| Equivalent Circuit Models (ECM) | Separable Natural Evolution Strategy (sNES) | Gravimetric Energy Density | -| | Gradient Descent | Volumetric Energy Density | +| Doyle-Fuller-Newman (DFN) | Exponential Natural Evolution Strategy (xNES) | Gaussian Log Likelihood | +| Many Particle Model (MPM) | Separable Natural Evolution Strategy (sNES) | Gaussian Log Likelihood w/ known variance | +| Multi-Species Multi-Reactants (MSMR) | Adaptive Moment Estimation with Weight Decay (AdamW) | Maximum a Posteriori (MAP) | +| Equivalent Circuit Models (ECM) | Improved Resilient Backpropagation (iRProp-) | Unscented Kalman Filter (UKF) | +| | SciPy Minimize & Differential Evolution | Gravimetric Energy Density | +| | Gradient Descent| Volumetric Energy Density | | | Nelder-Mead | | -| | SciPy Minimize & Differential Evolution | |

diff --git a/benchmarks/benchmark_parameterisation.py b/benchmarks/benchmark_parameterisation.py index 8440ba5e..a64116a4 100644 --- a/benchmarks/benchmark_parameterisation.py +++ b/benchmarks/benchmark_parameterisation.py @@ -13,7 +13,7 @@ class BenchmarkParameterisation: [ pybop.SciPyMinimize, pybop.SciPyDifferentialEvolution, - pybop.Adam, + pybop.AdamW, pybop.CMAES, pybop.GradientDescent, pybop.IRPropMin, diff --git a/benchmarks/benchmark_track_parameterisation.py b/benchmarks/benchmark_track_parameterisation.py index fac2d54a..9180ffec 100644 --- a/benchmarks/benchmark_track_parameterisation.py +++ b/benchmarks/benchmark_track_parameterisation.py @@ -13,7 +13,7 @@ class BenchmarkTrackParameterisation: [ pybop.SciPyMinimize, pybop.SciPyDifferentialEvolution, - pybop.Adam, + pybop.AdamW, pybop.CMAES, pybop.GradientDescent, pybop.IRPropMin, diff --git a/examples/notebooks/multi_optimiser_identification.ipynb b/examples/notebooks/multi_optimiser_identification.ipynb index 8bb6f353..f85b2609 100644 --- a/examples/notebooks/multi_optimiser_identification.ipynb +++ b/examples/notebooks/multi_optimiser_identification.ipynb @@ -279,7 +279,7 @@ "source": [ "### Selecting the Optimisers\n", "\n", - "Now, we can select the optimisers to investigate. The first object is a list of non-gradient-based PINTS's optimisers. The next object comprises the gradient-based PINTS's optimisers (Adam, GradientDescent, IRPropMin). The final object forms the SciPy optimisers which can have gradient and non-gradient-based algorithms." + "Now, we can select the optimisers to investigate. The first object is a list of non-gradient-based PINTS's optimisers. The next object comprises the gradient-based PINTS's optimisers (AdamW, GradientDescent, IRPropMin). The final object forms the SciPy optimisers which can have gradient and non-gradient-based algorithms." ] }, { @@ -296,7 +296,7 @@ "outputs": [], "source": [ "gradient_optimisers = [\n", - " pybop.Adam,\n", + " pybop.AdamW,\n", " pybop.GradientDescent,\n", " pybop.IRPropMin,\n", "]\n", @@ -343,8 +343,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Running Adam\n", - "NOTE: Boundaries ignored by Adam\n", + "Running AdamW\n", + "NOTE: Boundaries ignored by AdamW\n", "Running GradientDescent\n", "NOTE: Boundaries ignored by Gradient Descent\n", "Running IRPropMin\n" @@ -441,16 +441,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "| Optimiser: Adam | Results: [0.7940917 0.66244264] |\n", - "| Optimiser: Gradient descent | Results: [0.54971602 0.92692275] |\n", - "| Optimiser: iRprop- | Results: [0.72245096 0.67281911] |\n", - "| Optimiser: Covariance Matrix Adaptation Evolution Strategy (CMA-ES) | Results: [0.72101083 0.67313135] |\n", - "| Optimiser: Seperable Natural Evolution Strategy (SNES) | Results: [0.72082105 0.67317566] |\n", - "| Optimiser: Particle Swarm Optimisation (PSO) | Results: [0.71816152 0.6738613 ] |\n", - "| Optimiser: Exponential Natural Evolution Strategy (xNES) | Results: [0.72064411 0.67318804] |\n", - "| Optimiser: Nelder-Mead | Results: [0.72117726 0.67313287] |\n", - "| Optimiser: SciPyMinimize | Results: [0.62747904 0.7 ] |\n", - "| Optimiser: SciPyDifferentialEvolution | Results: [0.72103631 0.67312847] |\n" + "| Optimiser: AdamW | Results: [0.80186169 0.66943058] |\n", + "| Optimiser: Gradient descent | Results: [0.44491146 1.59642543] |\n", + "| Optimiser: iRprop- | Results: [0.8 0.66516386] |\n", + "| Optimiser: Covariance Matrix Adaptation Evolution Strategy (CMA-ES) | Results: [0.7999994 0.66516056] |\n", + "| Optimiser: Seperable Natural Evolution Strategy (SNES) | Results: [0.79672265 0.66566242] |\n", + "| Optimiser: Particle Swarm Optimisation (PSO) | Results: [0.79978922 0.66557426] |\n", + "| Optimiser: Exponential Natural Evolution Strategy (xNES) | Results: [0.79992605 0.66513294] |\n", + "| Optimiser: Nelder-Mead | Results: [0.81389091 0.66318217] |\n", + "| Optimiser: SciPyMinimize | Results: [0.63594266 0.7 ] |\n", + "| Optimiser: SciPyDifferentialEvolution | Results: [0.79999973 0.6651644 ] |\n" ] } ], @@ -509,7 +509,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelAdamTime / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelAdamWTime / sVoltage / V" ] }, "metadata": {}, @@ -518,7 +518,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.944.1ReferenceModelGradient descentTime / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelGradient descentTime / sVoltage / V" ] }, "metadata": {}, @@ -527,7 +527,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModeliRprop-Time / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModeliRprop-Time / sVoltage / V" ] }, "metadata": {}, @@ -536,7 +536,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelCovariance Matrix Adaptation Evolution Strategy (CMA-ES)Time / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelCovariance Matrix Adaptation Evolution Strategy (CMA-ES)Time / sVoltage / V" ] }, "metadata": {}, @@ -545,7 +545,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelSeperable Natural Evolution Strategy (SNES)Time / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelSeperable Natural Evolution Strategy (SNES)Time / sVoltage / V" ] }, "metadata": {}, @@ -554,7 +554,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelParticle Swarm Optimisation (PSO)Time / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelParticle Swarm Optimisation (PSO)Time / sVoltage / V" ] }, "metadata": {}, @@ -563,7 +563,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelExponential Natural Evolution Strategy (xNES)Time / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelExponential Natural Evolution Strategy (xNES)Time / sVoltage / V" ] }, "metadata": {}, @@ -572,7 +572,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelNelder-MeadTime / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelNelder-MeadTime / sVoltage / V" ] }, "metadata": {}, @@ -581,7 +581,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelSciPyMinimizeTime / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelSciPyMinimizeTime / sVoltage / V" ] }, "metadata": {}, @@ -590,7 +590,7 @@ { "data": { "image/svg+xml": [ - "05001000150020003.53.63.73.83.94ReferenceModelSciPyDifferentialEvolutionTime / sVoltage / V" + "02004006008003.753.83.853.93.9544.05ReferenceModelSciPyDifferentialEvolutionTime / sVoltage / V" ] }, "metadata": {}, @@ -627,7 +627,7 @@ { "data": { "image/svg+xml": [ - "5101520253000.511.522.533.5AdamIterationCost" + "51015202530012345AdamWIterationCost" ] }, "metadata": {}, @@ -636,7 +636,7 @@ { "data": { "image/svg+xml": [ - "01020300.60.650.70.750.80.8501020300.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "05101520250.60.650.70.750.80.8505101520250.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -645,7 +645,7 @@ { "data": { "image/svg+xml": [ - "5101520123456Gradient descentIterationCost" + "510152024681012141618Gradient descentIterationCost" ] }, "metadata": {}, @@ -654,7 +654,7 @@ { "data": { "image/svg+xml": [ - "0510152000.10.20.30.40.50.60.70.8051015200.40.50.60.70.80.911.11.21.3Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0510150510152025300510150.511.522.5Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -663,7 +663,7 @@ { "data": { "image/svg+xml": [ - "10203040506000.511.522.533.5iRprop-IterationCost" + "10203040012345iRprop-IterationCost" ] }, "metadata": {}, @@ -672,7 +672,7 @@ { "data": { "image/svg+xml": [ - "020400.650.70.750.8020400.50.550.60.65Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0102030400.60.650.70.750.80102030400.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -681,7 +681,7 @@ { "data": { "image/svg+xml": [ - "1020304000.511.522.53Covariance Matrix Adaptation Evolution Strategy (CMA-ES)IterationCost" + "1020304000.511.522.53Covariance Matrix Adaptation Evolution Strategy (CMA-ES)IterationCost" ] }, "metadata": {}, @@ -690,7 +690,7 @@ { "data": { "image/svg+xml": [ - "0501001502002500.50.550.60.650.70.750.80501001502002500.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0501001502002500.550.60.650.70.750.80501001502002500.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -699,7 +699,7 @@ { "data": { "image/svg+xml": [ - "10203040506000.511.52Seperable Natural Evolution Strategy (SNES)IterationCost" + "10203040506000.511.522.533.5Seperable Natural Evolution Strategy (SNES)IterationCost" ] }, "metadata": {}, @@ -708,7 +708,7 @@ { "data": { "image/svg+xml": [ - "01002003000.550.60.650.70.7501002003000.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "01002003000.550.60.650.70.750.801002003000.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -717,7 +717,7 @@ { "data": { "image/svg+xml": [ - "102030405000.511.5Particle Swarm Optimisation (PSO)IterationCost" + "1020304000.511.522.5Particle Swarm Optimisation (PSO)IterationCost" ] }, "metadata": {}, @@ -726,7 +726,7 @@ { "data": { "image/svg+xml": [ - "0501001502002500.50.550.60.650.70.750.80501001502002500.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0501001502000.50.550.60.650.70.750.80501001502000.40.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -735,7 +735,7 @@ { "data": { "image/svg+xml": [ - "1020304000.511.52Exponential Natural Evolution Strategy (xNES)IterationCost" + "10203040506000.511.522.5Exponential Natural Evolution Strategy (xNES)IterationCost" ] }, "metadata": {}, @@ -744,7 +744,7 @@ { "data": { "image/svg+xml": [ - "0501001502002500.60.650.70.750501001502002500.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "01002003000.60.650.70.750.801002003000.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -753,7 +753,7 @@ { "data": { "image/svg+xml": [ - "10203040506000.511.522.5Nelder-MeadIterationCost" + "10203040506000.511.522.533.5Nelder-MeadIterationCost" ] }, "metadata": {}, @@ -762,7 +762,7 @@ { "data": { "image/svg+xml": [ - "02040600.620.640.660.680.70.7202040600.50.550.60.650.70.75Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "02040600.60.650.70.750.80.8502040600.450.50.550.60.650.70.750.80.850.9Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -771,7 +771,7 @@ { "data": { "image/svg+xml": [ - "5101520253000.511.522.533.5SciPyMinimizeIterationCost" + "510152025012345SciPyMinimizeIterationCost" ] }, "metadata": {}, @@ -780,7 +780,7 @@ { "data": { "image/svg+xml": [ - "05101520250.610.620.630.640.650.660.670.680.6905101520250.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "05101520250.60.610.620.630.640.650.660.6705101520250.450.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -789,7 +789,7 @@ { "data": { "image/svg+xml": [ - "51015200.002950.0030.003050.00310.003150.00320.003250.0033SciPyDifferentialEvolutionIterationCost" + "510152025300.00360.00380.0040.00420.0044SciPyDifferentialEvolutionIterationCost" ] }, "metadata": {}, @@ -798,7 +798,7 @@ { "data": { "image/svg+xml": [ - "051015200.6950.70.7050.710.7150.72051015200.6730.6740.6750.6760.6770.6780.679Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "05101520250.7550.760.7650.770.7750.780.7850.790.7950.805101520250.6650.6660.6670.6680.6690.67Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -835,7 +835,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4AdamNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5AdamWNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -844,7 +844,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4Gradient descentNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5Gradient descentNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -853,7 +853,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4iRprop-Negative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5iRprop-Negative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -862,7 +862,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4Covariance Matrix Adaptation Evolution Strategy (CMA-ES)Negative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5Covariance Matrix Adaptation Evolution Strategy (CMA-ES)Negative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -871,7 +871,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4Seperable Natural Evolution Strategy (SNES)Negative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5Seperable Natural Evolution Strategy (SNES)Negative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -880,7 +880,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4Particle Swarm Optimisation (PSO)Negative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5Particle Swarm Optimisation (PSO)Negative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -889,7 +889,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4Exponential Natural Evolution Strategy (xNES)Negative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5Exponential Natural Evolution Strategy (xNES)Negative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -898,7 +898,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4Nelder-MeadNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5Nelder-MeadNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -907,7 +907,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4SciPyMinimizeNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5SciPyMinimizeNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -916,7 +916,7 @@ { "data": { "image/svg+xml": [ - "0.50.550.60.650.70.750.80.550.60.650.70.750.80.40.81.21.622.4SciPyDifferentialEvolutionNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.550.60.650.70.750.80.50.550.60.650.70.750.80.511.522.533.5SciPyDifferentialEvolutionNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, diff --git a/examples/notebooks/spm_AdamW.ipynb b/examples/notebooks/spm_AdamW.ipynb new file mode 100644 index 00000000..20b73330 --- /dev/null +++ b/examples/notebooks/spm_AdamW.ipynb @@ -0,0 +1,826 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "expmkveO04pw" + }, + "source": [ + "## Parameter Estimation with AdamW in PyBOP\n", + "\n", + "In this notebook, we demonstrate an example of parameter estimation for a single-particle model using the AdamW optimiser [1][2]. The AdamW optimiser is an algorithm for gradient-based optimisation, combining the advantages of the Adaptive Gradient Algorithm (AdaGrad) and Root Mean Square Propagation (RMSProp).\n", + "\n", + "[[1]: Adam: A Method for Stochastic Optimization](https://arxiv.org/abs/1412.6980) \n", + "\n", + "[[2]: Decoupled Weight Decay Regularization](https://doi.org/10.48550/arXiv.1711.05101)\n", + "\n", + "### Setting up the Environment\n", + "\n", + "Before we begin, we need to ensure that we have all the necessary tools. We will install PyBOP from its development branch and upgrade some dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "execution": { + "iopub.execute_input": "2024-04-04T13:51:40.337833Z", + "iopub.status.busy": "2024-04-04T13:51:40.337689Z", + "iopub.status.idle": "2024-04-04T13:51:41.935008Z", + "shell.execute_reply": "2024-04-04T13:51:41.934618Z" + }, + "id": "X87NUGPW04py", + "outputId": "0d785b07-7cff-4aeb-e60a-4ff5a669afbf" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: pip in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (24.0)\n", + "Requirement already satisfied: ipywidgets in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (8.1.2)\n", + "Requirement already satisfied: comm>=0.1.3 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (0.2.1)\n", + "Requirement already satisfied: ipython>=6.1.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (8.22.1)\n", + "Requirement already satisfied: traitlets>=4.3.1 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (5.14.1)\n", + "Requirement already satisfied: widgetsnbextension~=4.0.10 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (4.0.10)\n", + "Requirement already satisfied: jupyterlab-widgets~=3.0.10 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipywidgets) (3.0.10)\n", + "Requirement already satisfied: decorator in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (5.1.1)\n", + "Requirement already satisfied: jedi>=0.16 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.1)\n", + "Requirement already satisfied: matplotlib-inline in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.6)\n", + "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.43)\n", + "Requirement already satisfied: pygments>=2.4.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (2.17.2)\n", + "Requirement already satisfied: stack-data in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\n", + "Requirement already satisfied: pexpect>4.3 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\n", + "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.3)\n", + "Requirement already satisfied: ptyprocess>=0.5 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\n", + "Requirement already satisfied: wcwidth in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\n", + "Requirement already satisfied: executing>=1.2.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.0.1)\n", + "Requirement already satisfied: asttokens>=2.1.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.4.1)\n", + "Requirement already satisfied: pure-eval in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (0.2.2)\n", + "Requirement already satisfied: six>=1.12.0 in /Users/engs2510/.pyenv/versions/3.11.7/envs/pybop/lib/python3.11/site-packages (from asttokens>=2.1.0->stack-data->ipython>=6.1.0->ipywidgets) (1.16.0)\n", + "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\n", + "%pip install pybop -q" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jAvD5fk104p0" + }, + "source": [ + "### Importing Libraries\n", + "\n", + "With the environment set up, we can now import PyBOP alongside other libraries we will need:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:41.936561Z", + "iopub.status.busy": "2024-04-04T13:51:41.936439Z", + "iopub.status.idle": "2024-04-04T13:51:42.508083Z", + "shell.execute_reply": "2024-04-04T13:51:42.507654Z" + }, + "id": "SQdt4brD04p1" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "import pybop" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5XU-dMtU04p2" + }, + "source": [ + "### Generate Synthetic Data\n", + "\n", + "To demonstrate parameter estimation, we first need some data. We will generate synthetic data using the PyBOP forward model, which requires defining a parameter set and the model itself.\n", + "\n", + "#### Defining Parameters and Model\n", + "\n", + "We start by creating an example parameter set and then instantiate the single-particle model (SPM):" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.509591Z", + "iopub.status.busy": "2024-04-04T13:51:42.509437Z", + "iopub.status.idle": "2024-04-04T13:51:42.534794Z", + "shell.execute_reply": "2024-04-04T13:51:42.534452Z" + } + }, + "outputs": [], + "source": [ + "parameter_set = pybop.ParameterSet.pybamm(\"Chen2020\")\n", + "model = pybop.lithium_ion.SPM(parameter_set=parameter_set)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Simulating Forward Model\n", + "\n", + "We can then simulate the model using the `predict` method, with a default constant current to generate voltage data." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.536154Z", + "iopub.status.busy": "2024-04-04T13:51:42.536069Z", + "iopub.status.idle": "2024-04-04T13:51:42.610305Z", + "shell.execute_reply": "2024-04-04T13:51:42.609892Z" + }, + "id": "sBasxv8U04p3" + }, + "outputs": [], + "source": [ + "t_eval = np.arange(0, 900, 2)\n", + "values = model.predict(t_eval=t_eval)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding Noise to Voltage Data\n", + "\n", + "To make the parameter estimation more realistic, we add Gaussian noise to the data." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.611946Z", + "iopub.status.busy": "2024-04-04T13:51:42.611728Z", + "iopub.status.idle": "2024-04-04T13:51:42.621525Z", + "shell.execute_reply": "2024-04-04T13:51:42.621156Z" + } + }, + "outputs": [], + "source": [ + "sigma = 0.001\n", + "corrupt_values = values[\"Voltage [V]\"].data + np.random.normal(0, sigma, len(t_eval))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X8-tubYY04p_" + }, + "source": [ + "## Identify the Parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PQqhvSZN04p_" + }, + "source": [ + "We will now set up the parameter estimation process by defining the datasets for optimisation and selecting the model parameters we wish to estimate." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating Optimisation Dataset\n", + "\n", + "The dataset for optimisation is composed of time, current, and the noisy voltage data:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.622671Z", + "iopub.status.busy": "2024-04-04T13:51:42.622478Z", + "iopub.status.idle": "2024-04-04T13:51:42.628864Z", + "shell.execute_reply": "2024-04-04T13:51:42.628519Z" + }, + "id": "zuvGHWID04p_" + }, + "outputs": [], + "source": [ + "dataset = pybop.Dataset(\n", + " {\n", + " \"Time [s]\": t_eval,\n", + " \"Current function [A]\": values[\"Current [A]\"].data,\n", + " \"Voltage [V]\": corrupt_values,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ffS3CF_704qA" + }, + "source": [ + "### Defining Parameters to Estimate\n", + "\n", + "We select the parameters for estimation and set up their prior distributions and bounds:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.629987Z", + "iopub.status.busy": "2024-04-04T13:51:42.629809Z", + "iopub.status.idle": "2024-04-04T13:51:42.631895Z", + "shell.execute_reply": "2024-04-04T13:51:42.631621Z" + }, + "id": "WPCybXIJ04qA" + }, + "outputs": [], + "source": [ + "parameters = [\n", + " pybop.Parameter(\n", + " \"Negative electrode active material volume fraction\",\n", + " prior=pybop.Gaussian(0.6, 0.02),\n", + " bounds=[0.5, 0.8],\n", + " ),\n", + " pybop.Parameter(\n", + " \"Positive electrode active material volume fraction\",\n", + " prior=pybop.Gaussian(0.48, 0.02),\n", + " bounds=[0.4, 0.7],\n", + " ),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n4OHa-aF04qA" + }, + "source": [ + "### Setting up the Optimisation Problem\n", + "\n", + "With the datasets and parameters defined, we can set up the optimisation problem, its cost function, and the optimiser." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.632931Z", + "iopub.status.busy": "2024-04-04T13:51:42.632782Z", + "iopub.status.idle": "2024-04-04T13:51:42.705454Z", + "shell.execute_reply": "2024-04-04T13:51:42.705066Z" + }, + "id": "etMzRtx404qA" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NOTE: Boundaries ignored by AdamW\n" + ] + } + ], + "source": [ + "problem = pybop.FittingProblem(model, parameters, dataset)\n", + "cost = pybop.SumSquaredError(problem)\n", + "optim = pybop.Optimisation(cost, optimiser=pybop.AdamW)\n", + "optim.set_max_unchanged_iterations(40)\n", + "optim.set_max_iterations(150)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "caprp-bV04qB" + }, + "source": [ + "### Running the Optimisation\n", + "\n", + "We proceed to run the AdamW optimisation algorithm to estimate the parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:42.706564Z", + "iopub.status.busy": "2024-04-04T13:51:42.706469Z", + "iopub.status.idle": "2024-04-04T13:51:50.537424Z", + "shell.execute_reply": "2024-04-04T13:51:50.537032Z" + }, + "id": "-9OVt0EQ04qB" + }, + "outputs": [], + "source": [ + "x, final_cost = optim.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-4pZsDmS04qC" + }, + "source": [ + "### Viewing the Estimated Parameters\n", + "\n", + "After the optimisation, we can examine the estimated parameter values:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "execution": { + "iopub.execute_input": "2024-04-04T13:51:50.538815Z", + "iopub.status.busy": "2024-04-04T13:51:50.538619Z", + "iopub.status.idle": "2024-04-04T13:51:50.541683Z", + "shell.execute_reply": "2024-04-04T13:51:50.541465Z" + }, + "id": "Hgz8SV4i04qC", + "outputId": "e1e42ae7-5075-4c47-dd68-1b22ecc170f6" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.76334915, 0.66225839])" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x # This will output the estimated parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KxKURtH704qC" + }, + "source": [ + "## Plotting and Visualisation\n", + "\n", + "PyBOP provides various plotting utilities to visualise the results of the optimisation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-cWCOiqR04qC" + }, + "source": [ + "### Comparing System Response\n", + "\n", + "We can quickly plot the system's response using the estimated parameters compared to the target:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 467 + }, + "execution": { + "iopub.execute_input": "2024-04-04T13:51:50.542618Z", + "iopub.status.busy": "2024-04-04T13:51:50.542472Z", + "iopub.status.idle": "2024-04-04T13:51:50.986055Z", + "shell.execute_reply": "2024-04-04T13:51:50.985844Z" + }, + "id": "tJUJ80Ve04qD", + "outputId": "855fbaa2-1e09-4935-eb1a-8caf7f99eb75" + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "02004006008003.83.853.93.9544.05ReferenceModelOptimised ComparisonTime / sVoltage / V" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pybop.quick_plot(problem, parameter_values=x, title=\"Optimised Comparison\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Convergence and Parameter Trajectories\n", + "\n", + "To assess the optimisation process, we can plot the convergence of the cost function and the trajectories of the parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:50.987237Z", + "iopub.status.busy": "2024-04-04T13:51:50.986963Z", + "iopub.status.idle": "2024-04-04T13:51:52.766386Z", + "shell.execute_reply": "2024-04-04T13:51:52.766178Z" + }, + "id": "N5XYkevi04qD" + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "2040608010012014000.511.522.533.54ConvergenceIterationCost" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "0501000.60.650.70.750.80.850501000.50.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pybop.plot_convergence(optim)\n", + "pybop.plot_parameters(optim);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cost Landscape\n", + "\n", + "Finally, we can visualise the cost landscape and the path taken by the optimiser:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2024-04-04T13:51:52.767346Z", + "iopub.status.busy": "2024-04-04T13:51:52.767261Z", + "iopub.status.idle": "2024-04-04T13:51:57.666000Z", + "shell.execute_reply": "2024-04-04T13:51:57.665745Z" + } + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "0.50.550.60.650.70.750.80.40.450.50.550.60.650.70246810Cost LandscapeNegative electrode active material volume fractionPositive electrode active material volume fraction" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "0.60.650.70.750.80.850.90.50.550.60.650.70.750.80.40.81.21.622.4Cost LandscapeNegative electrode active material volume fractionPositive electrode active material volume fraction" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the cost landscape\n", + "pybop.plot2d(cost, steps=15)\n", + "# Plot the cost landscape with optimisation path and updated bounds\n", + "bounds = np.array([[0.6, 0.9], [0.5, 0.8]])\n", + "pybop.plot2d(optim, bounds=bounds, steps=15);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Conclusion\n", + "\n", + "This notebook illustrates how to perform parameter estimation using AdamW in PyBOP, providing insights into the optimisation process through various visualisations." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "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", + "version": "3.11.7" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "06f2374f91c8455bb63252092512f2ed": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "423bffea3a1c42b49a9ad71218e5811b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "56ff19291e464d63b23e63b8e2ac9ea3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "SliderStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "SliderStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "description_width": "", + "handle_color": null + } + }, + "646a8670cb204a31bb56bc2380898093": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7d46516469314b88be3500e2afcafcf6": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_646a8670cb204a31bb56bc2380898093", + "msg_id": "", + "outputs": [], + "tabbable": null, + "tooltip": null + } + }, + "8d003c14da5f4fa68284b28c15cee6e6": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [ + "widget-interact" + ], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_aef2fa7adcc14ad0854b73d5910ae3b4", + "IPY_MODEL_7d46516469314b88be3500e2afcafcf6" + ], + "layout": "IPY_MODEL_423bffea3a1c42b49a9ad71218e5811b", + "tabbable": null, + "tooltip": null + } + }, + "aef2fa7adcc14ad0854b73d5910ae3b4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "FloatSliderModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "FloatSliderModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "FloatSliderView", + "behavior": "drag-tap", + "continuous_update": true, + "description": "t", + "description_allow_html": false, + "disabled": false, + "layout": "IPY_MODEL_06f2374f91c8455bb63252092512f2ed", + "max": 1.1333333333333333, + "min": 0, + "orientation": "horizontal", + "readout": true, + "readout_format": ".2f", + "step": 0.011333333333333332, + "style": "IPY_MODEL_56ff19291e464d63b23e63b8e2ac9ea3", + "tabbable": null, + "tooltip": null, + "value": 0 + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/scripts/spm_adam.py b/examples/scripts/spm_AdamW.py similarity index 98% rename from examples/scripts/spm_adam.py rename to examples/scripts/spm_AdamW.py index 82f884e2..10351512 100644 --- a/examples/scripts/spm_adam.py +++ b/examples/scripts/spm_AdamW.py @@ -54,7 +54,7 @@ def noise(sigma): model, parameters, dataset, signal=signal, init_soc=init_soc ) cost = pybop.RootMeanSquaredError(problem) -optim = pybop.Adam( +optim = pybop.AdamW( cost, verbose=True, allow_infeasible_solutions=True, diff --git a/pybop/__init__.py b/pybop/__init__.py index 6d2c0dbf..61971e95 100644 --- a/pybop/__init__.py +++ b/pybop/__init__.py @@ -102,6 +102,8 @@ # # Optimiser class # + +from .optimisers._adamw import AdamWImpl from .optimisers.base_optimiser import BaseOptimiser, Result from .optimisers.base_pints_optimiser import BasePintsOptimiser from .optimisers.scipy_optimisers import ( @@ -118,6 +120,7 @@ PSO, SNES, XNES, + AdamW, ) from .optimisers.optimisation import Optimisation diff --git a/pybop/optimisers/_adamw.py b/pybop/optimisers/_adamw.py new file mode 100644 index 00000000..24e5ec98 --- /dev/null +++ b/pybop/optimisers/_adamw.py @@ -0,0 +1,235 @@ +# +# Extends the Pints' Adam Class with a Weight Decay addition +# + +import numpy as np +from pints import Optimiser as PintsOptimiser + + +class AdamWImpl(PintsOptimiser): + """ + AdamW optimiser (adaptive moment estimation with weight decay), as described in [1]_. + + This method is an extension of the Adam optimiser that introduces weight decay, + which helps to regularise the weights and prevent overfitting. + + This class reimplements the Pints' Adam Optimiser, but with the weight decay + functionality mentioned above. Original creation and credit is attributed to Pints. + + Pseudo-code is given below. Here the value of the j-th parameter at + iteration i is given as ``p_j[i]`` and the corresponding derivative is + denoted ``g_j[i]``:: + + m_j[i] = beta1 * m_j[i - 1] + (1 - beta1) * g_j[i] + v_j[i] = beta2 * v_j[i - 1] + (1 - beta2) * g_j[i]**2 + + m_j' = m_j[i] / (1 - beta1**(1 + i)) + v_j' = v_j[i] / (1 - beta2**(1 + i)) + + p_j[i] = p_j[i - 1] - alpha * (m_j' / (sqrt(v_j') + eps) + lambda * p_j[i - 1]) + + The initial values of the moments are ``m_j[0] = v_j[0] = 0``, after which + they decay with rates ``beta1`` and ``beta2``. The default values for these are, + ``beta1 = 0.9`` and ``beta2 = 0.999``. + + The terms ``m_j'`` and ``v_j'`` are "initialisation bias corrected" + versions of ``m_j`` and ``v_j`` (see section 2 of the paper). + + The parameter ``alpha`` is a step size, which is set as ``min(sigma0)`` in + this implementation. + + The parameter ``lambda`` is the weight decay rate, which is set to ``0.01`` + by default in this implementation. + + Finally, ``eps`` is a small constant used to avoid division by zero, set to + ``eps = `np.finfo(float).eps` in this implementation. + + This is an unbounded method: Any ``boundaries`` will be ignored. + + References + ---------- + .. [1] Decoupled Weight Decay Regularization + Loshchilov and Hutter, 2019, arxiv (version v3) + https://doi.org/10.48550/arXiv.1711.05101 + """ + + def __init__(self, x0, sigma0=0.015, boundaries=None): + if boundaries is not None: + print("NOTE: Boundaries ignored by AdamW") + + self.boundaries = None + super().__init__(x0, sigma0, self.boundaries) + + # Set optimiser state + self._running = False + self._ready_for_tell = False + + # Best solution found + self._x_best = self._x0 + self._f_best = np.inf + + # Current point, score, and gradient + self._current = self._x0 + self._current_f = np.inf + self._current_df = None + + # Proposed next point (read-only, so can be passed to user) + self._proposed = self._x0 + self._proposed.setflags(write=False) + + # Moment vectors + self._m = np.zeros(self._x0.shape) + self._v = np.zeros(self._x0.shape) + + # Exponential decay rates for the moment estimates + self._b1 = 0.9 + self._b2 = 0.999 + + # Step size + self._alpha = np.min(self._sigma0) + + # Weight decay rate + self.set_lambda() + + # Small number added to avoid divide-by-zero + self._eps = np.finfo(float).eps + + # Powers of decay rates + self._b1t = 1 + self._b2t = 1 + + def ask(self): + """ + Returns a list of next points in the parameter-space + to evaluate from the optimiser. + """ + + # Running, and ready for tell now + self._ready_for_tell = True + self._running = True + + # Return proposed points (just the one) + return [self._proposed] + + def f_best(self): + """ + Returns the best score found so far. + """ + return self._f_best + + def f_guessed(self): + """ + Returns the score of the last guessed point. + """ + return self._current_f + + def name(self): + """ + Returns the name of the optimiser. + """ + return "AdamW" + + def needs_sensitivities(self): + """ + Returns ``False`` if this optimiser does not require gradient, + and ``True`` otherwise. + """ + return True + + def n_hyper_parameters(self): + """ + The number of hyper-parameters used by this optimiser. + """ + return 5 + + def running(self): + """ + Returns ``True`` if the optimisation is in progress. + """ + return self._running + + def tell(self, reply): + """ + Receives a list of function values from the cost function from points + previously specified by `self.ask()`, and updates the optimiser state + accordingly. + """ + + # Check ask-tell pattern + if not self._ready_for_tell: + raise Exception("ask() not called before tell()") + self._ready_for_tell = False + + # Unpack reply + fx, dfx = reply[0] + + # Update current point + self._current = self._proposed + self._current_f = fx + self._current_df = dfx + + # Update bx^t + self._b1t *= self._b1 + self._b2t *= self._b2 + + # "Update biased first moment estimate" + self._m = self._b1 * self._m + (1 - self._b1) * dfx + + # "Update biased second raw moment estimate" + self._v = self._b2 * self._v + (1 - self._b2) * dfx**2 + + # "Compute bias-corrected first moment estimate" + m = self._m / (1 - self._b1t) + + # "Compute bias-corrected second raw moment estimate" + v = self._v / (1 - self._b2t) + + # Take step with weight decay + self._proposed = self._current - self._alpha * ( + m / (np.sqrt(v) + self._eps) + self._lambda * self._current + ) + + # Update x_best and f_best + if self._f_best > fx: + self._f_best = fx + self._x_best = self._current + + def x_best(self): + """ + Returns the best parameter values found so far. + """ + return self._x_best + + def x_guessed(self): + """ + Returns the last guessed parameter values. + """ + return self._current + + def set_lambda(self, lambda_: float = 0.01) -> None: + """ + Sets the lambda_ decay constant. This is the weight decay rate + that helps in finding the optimal solution. + """ + if not isinstance(lambda_, (int, float)) or not 0 < lambda_ <= 1: + raise ValueError("lambda_ must be a numeric value between 0 and 1.") + + self._lambda = float(lambda_) + + def set_b1(self, b1: float) -> None: + """ + Sets the b1 momentum decay constant. + """ + if not isinstance(b1, (int, float)) or not 0 < b1 <= 1: + raise ValueError("b1 must be a numeric value between 0 and 1.") + + self._b1 = float(b1) + + def set_b2(self, b2: float) -> None: + """ + Sets the b2 momentum decay constant. + """ + if not isinstance(b2, (int, float)) or not 0 < b2 <= 1: + raise ValueError("b2 must be a numeric value between 0 and 1.") + + self._b2 = float(b2) diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index d2e55371..4872973a 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -1,3 +1,5 @@ +from warnings import warn + from pints import CMAES as PintsCMAES from pints import PSO as PintsPSO from pints import SNES as PintsSNES @@ -7,7 +9,7 @@ from pints import IRPropMin as PintsIRPropMin from pints import NelderMead as PintsNelderMead -from pybop import BasePintsOptimiser +from pybop import AdamWImpl, BasePintsOptimiser class GradientDescent(BasePintsOptimiser): @@ -60,10 +62,43 @@ class Adam(BasePintsOptimiser): pints.Adam : The PINTS implementation this class is based on. """ + warn( + "Adam is deprecated and will be removed in a future release. Please use AdamW instead.", + DeprecationWarning, + stacklevel=2, + ) + def __init__(self, cost, **optimiser_kwargs): super().__init__(cost, PintsAdam, **optimiser_kwargs) +class AdamW(BasePintsOptimiser): + """ + Implements the AdamW optimisation algorithm in PyBOP. + + This class extends the AdamW optimiser, which is a variant of the Adam + optimiser that incorporates weight decay. AdamW is designed to be more + robust and stable for training deep neural networks, particularly when + using larger learning rates. + + Parameters + ---------- + **optimiser_kwargs : optional + Valid PyBOP option keys and their values, for example: + x0 : array_like + Initial position from which optimisation will start. + sigma0 : float + Initial step size. + + See Also + -------- + pybop.AdamWImpl : The PyBOP implementation this class is based on. + """ + + def __init__(self, cost, **optimiser_kwargs): + super().__init__(cost, AdamWImpl, **optimiser_kwargs) + + class IRPropMin(BasePintsOptimiser): """ Implements the iRpropMin optimization algorithm. diff --git a/tests/integration/test_spm_parameterisations.py b/tests/integration/test_spm_parameterisations.py index 6e5108d7..9ae2b421 100644 --- a/tests/integration/test_spm_parameterisations.py +++ b/tests/integration/test_spm_parameterisations.py @@ -81,7 +81,7 @@ def spm_costs(self, model, parameters, cost_class, init_soc): "optimiser", [ pybop.SciPyDifferentialEvolution, - pybop.Adam, + pybop.AdamW, pybop.CMAES, pybop.IRPropMin, pybop.NelderMead, diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index 2eef89ad..97fe12fc 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -73,6 +73,7 @@ def two_param_cost(self, model, two_parameters, dataset): (pybop.SciPyDifferentialEvolution, "SciPyDifferentialEvolution"), (pybop.GradientDescent, "Gradient descent"), (pybop.Adam, "Adam"), + (pybop.AdamW, "AdamW"), (pybop.CMAES, "Covariance Matrix Adaptation Evolution Strategy (CMA-ES)"), (pybop.SNES, "Seperable Natural Evolution Strategy (SNES)"), (pybop.XNES, "Exponential Natural Evolution Strategy (xNES)"), @@ -110,6 +111,7 @@ def test_optimiser_classes(self, two_param_cost, optimiser, expected_name): pybop.SciPyDifferentialEvolution, pybop.GradientDescent, pybop.Adam, + pybop.AdamW, pybop.SNES, pybop.XNES, pybop.PSO, @@ -210,6 +212,39 @@ def test_optimiser_kwargs(self, cost, optimiser): ): optimiser(cost=cost, bounds={"upper": [np.inf], "lower": [0.57]}) + # Test AdamW hyperparameters + if optimiser in [pybop.AdamW]: + optim = optimiser(cost=cost, b1=0.9, b2=0.999, lambda_=0.1) + optim.pints_optimiser.set_b1(0.9) + optim.pints_optimiser.set_b2(0.9) + optim.pints_optimiser.set_lambda(0.1) + + assert optim.pints_optimiser._b1 == 0.9 + assert optim.pints_optimiser._b2 == 0.9 + assert optim.pints_optimiser._lambda == 0.1 + + # Incorrect values + for i, match in (("Value", -1),): + with pytest.raises( + Exception, match="must be a numeric value between 0 and 1." + ): + optim.pints_optimiser.set_b1(i) + with pytest.raises( + Exception, match="must be a numeric value between 0 and 1." + ): + optim.pints_optimiser.set_b2(i) + with pytest.raises( + Exception, match="must be a numeric value between 0 and 1." + ): + optim.pints_optimiser.set_lambda(i) + + # Check defaults + assert optim.pints_optimiser.n_hyper_parameters() == 5 + assert not optim.pints_optimiser.running() + assert optim.pints_optimiser.x_guessed() == optim.pints_optimiser._x0 + with pytest.raises(Exception): + optim.pints_optimiser.tell([0.1]) + else: # Check and update initial values assert optim.x0 == cost.x0