Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callback #1880

Merged
merged 17 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
# [Unreleased](https://github.com/pybamm-team/PyBaMM/)

# [v22.4](https://github.com/pybamm-team/PyBaMM/tree/v22.4) - 2022-04-30

## Features

- Added a casadi version of the IDKLU solver, which is used for `model.convert_to_format = "casadi"` ([#2002](https://github.com/pybamm-team/PyBaMM/pull/2002))
- Added functionality to generate Julia expressions from a model. See [PyBaMM.jl](https://github.com/tinosulzer/PyBaMM.jl) for how to use these ([#1942](https://github.com/pybamm-team/PyBaMM/pull/1942)))
- Added basic callbacks to the Simulation class, and a LoggingCallback ([#1880](https://github.com/pybamm-team/PyBaMM/pull/1880)))

## Bug fixes

- Corrected legend order in "plot_voltage_components.py", so each entry refers to the correct overpotential. ([#2061](https://github.com/pybamm-team/PyBaMM/pull/2061))
- Remove old deprecation errors, including those in `parameter_values.py` that caused the simulation if, for example, the reaction rate is re-introduced manually ([#2022](https://github.com/pybamm-team/PyBaMM/pull/2022))

## Breaking changes

- Changed domain-specific parameter names to a nested attribute, e.g. `param.c_n_max` is now `param.n.c_max` ([#2063](https://github.com/pybamm-team/PyBaMM/pull/2063))

# [v22.4](https://github.com/pybamm-team/PyBaMM/tree/v22.4) - 2022-04-30

## Features

- Added a casadi version of the IDKLU solver, which is used for `model.convert_to_format = "casadi"` ([#2002](https://github.com/pybamm-team/PyBaMM/pull/2002))

## Bug fixes

- Remove old deprecation errors, including those in `parameter_values.py` that caused the simulation if, for example, the reaction rate is re-introduced manually ([#2022](https://github.com/pybamm-team/PyBaMM/pull/2022))

# [v22.3](https://github.com/pybamm-team/PyBaMM/tree/v22.3) - 2022-03-31

## Features
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ API documentation
source/simulation
source/plotting/index
source/util
source/callbacks
source/citations
source/parameters_cli
source/batch_study
Expand Down
13 changes: 13 additions & 0 deletions docs/source/callbacks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Callbacks
=========

.. autoclass:: pybamm.callbacks.Callback
:members:

.. autoclass:: pybamm.callbacks.CallbackList
:members:

.. autoclass:: pybamm.callbacks.LoggingCallback
:members:

.. autofunction:: pybamm.callbacks.setup_callbacks
2 changes: 1 addition & 1 deletion examples/notebooks/batch_study.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.0"
"version": "3.8.12"
},
"toc": {
"base_numbering": 1,
Expand Down
303 changes: 303 additions & 0 deletions examples/notebooks/callbacks.ipynb

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pybamm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
have_julia,
)
from .logger import logger, set_logging_level
from .logger import logger, set_logging_level, get_new_logger
from .settings import settings
from .citations import Citations, citations, print_citations

Expand Down Expand Up @@ -239,6 +240,11 @@
#
from .batch_study import BatchStudy

#
# Callbacks
#
from . import callbacks

#
# Remove any imported modules, so we don't expose them as part of pybamm
#
Expand Down
243 changes: 243 additions & 0 deletions pybamm/callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#
# Base class for callbacks and some useful callbacks for pybamm
# Callbacks are used to perform actions (e.g. logging, saving)
# at certain points in the simulation
# Inspired by Keras callbacks
# https://github.com/keras-team/keras/blob/master/keras/callbacks.py
#
import pybamm
import numpy as np
import inspect


def setup_callbacks(callbacks):
callbacks = callbacks or []
if not isinstance(callbacks, list):
callbacks = [callbacks]

# Check if there is a logging callback already, if not add the default one
has_logging_callback = any(isinstance(cb, LoggingCallback) for cb in callbacks)
if not has_logging_callback:
callbacks.append(LoggingCallback())

return CallbackList(callbacks)


class Callback:
"""
Base class for callbacks, for documenting callback methods.

Callbacks are used to perform actions (e.g. logging, saving) at certain points in
the simulation. Each callback method is named `on_<event>`, where `<event>`
describes the point at which the callback is called. For example, the callback
`on_experiment_start` is called at the start of an experiment simulation. In
general, callbacks take a single argument, `logs`, which is a dictionary of
information about the simulation. Each callback method should return `None`
(the output of the method is ignored).

**EXPERIMENTAL** - this class is experimental and the callback interface may
change in future releases.
"""

def on_experiment_start(self, logs):
"""
Called at the start of an experiment simulation.
"""
pass

def on_cycle_start(self, logs):
"""
Called at the start of each cycle in an experiment simulation.
"""
pass

def on_step_start(self, logs):
"""
Called at the start of each step in an experiment simulation.
"""
pass

def on_step_end(self, logs):
"""
Called at the end of each step in an experiment simulation.
"""
pass

def on_cycle_end(self, logs):
"""
Called at the end of each cycle in an experiment simulation.
"""
pass

def on_experiment_end(self, logs):
"""
Called at the end of an experiment simulation.
"""
pass

def on_experiment_error(self, logs):
"""
Called when a SolverError occurs during an experiment simulation.

For example, this could be used to send an error alert with a bug report when
running batch simulations in the cloud.
"""
pass

def on_experiment_infeasible(self, logs):
"""
Called when an experiment simulation is infeasible.
"""
pass


########################################################################################
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved
class CallbackList(Callback):
"""
Container abstracting a list of callbacks, so that they can be called in a
single step e.g. `callbacks.on_simulation_end(...)`.

This is done without having to redefine the method each time by using the
`callback_loop_decorator` decorator, which is applied to every method that starts
with `on_`, using the `inspect` module. See
https://stackoverflow.com/questions/1367514/how-to-decorate-a-method-inside-a-class.

If better control over how the callbacks are called is required, it might be better
to be more explicit with the for loop.
"""

def __init__(self, callbacks):
self.callbacks = callbacks

def __len__(self):
return len(self.callbacks)

def __getitem__(self, index):
return self.callbacks[index]


def callback_loop_decorator(func):
"""
A decorator to call the function on every callback in `self.callbacks`
"""

def wrapper(self, *args, **kwargs):
for callback in self.callbacks:
# call the function on the callback
getattr(callback, func.__name__)(*args, **kwargs)

return wrapper


# inspect.getmembers finds all the methods in the Callback class
for name, func in inspect.getmembers(CallbackList, inspect.isfunction):
if name.startswith("on_"):
# Replaces each function with the decorated version
setattr(CallbackList, name, callback_loop_decorator(func))

########################################################################################


class LoggingCallback(Callback):
"""
Logging callback, implements methods to log progress of the simulation.

Parameters
----------
logfile : str, optional
Where to send the log output. If None, uses pybamm's logger.

**Extends:** :class:`pybamm.callbacks.Callback`
"""

def __init__(self, logfile=None):
self.logfile = logfile
if logfile is None:
# Use pybamm's logger, which prints to command line
self.logger = pybamm.logger
else:
# Use a custom logger, this will have its own level so set it to the same
# level as the pybamm logger (users can override this)
self.logger = pybamm.get_new_logger(__name__, logfile)
self.logger.setLevel(pybamm.logger.level)

def on_experiment_start(self, logs):
# Clear the log file
self.logger.info("Start running experiment")
if self.logfile is not None:
with open(self.logfile, "w") as f:
f.write("")

def on_cycle_start(self, logs):
cycle_num, num_cycles = logs["cycle number"]
total_time = logs["elapsed time"]
self.logger.notice(
f"Cycle {cycle_num}/{num_cycles} ({total_time} elapsed) " + "-" * 20
)

def on_step_start(self, logs):
cycle_num, num_cycles = logs["cycle number"]
step_num, cycle_length = logs["step number"]
operating_conditions = logs["step operating conditions"]
self.logger.notice(
f"Cycle {cycle_num}/{num_cycles}, step {step_num}/{cycle_length}: "
f"{operating_conditions}"
)

def on_step_end(self, logs):
pass

def on_cycle_end(self, logs):
cap_stop = logs["stopping conditions"]["capacity"]
if cap_stop is not None:
cap_now = logs["summary variables"]["Capacity [A.h]"]
cap_start = logs["start capacity"]
if np.isnan(cap_now) or cap_now > cap_stop:
self.logger.notice(
f"Capacity is now {cap_now:.3f} Ah (originally {cap_start:.3f} Ah, "
f"will stop at {cap_stop:.3f} Ah)"
)
else:
self.logger.notice(
f"Stopping experiment since capacity ({cap_now:.3f} Ah) "
f"is below stopping capacity ({cap_stop:.3f} Ah)."
)

voltage_stop = logs["stopping conditions"]["voltage"]
if voltage_stop is not None:
min_voltage = logs["summary variables"]["Minimum voltage [V]"]
if min_voltage > voltage_stop[0]:
self.logger.notice(
f"Minimum voltage is now {min_voltage:.3f} V "
f"(will stop at {voltage_stop[0]:.3f} V)"
)
else:
self.logger.notice(
f"Stopping experiment since minimum voltage ({min_voltage:.3f} V) "
f"is below stopping voltage ({voltage_stop[0]:.3f} V)."
)

def on_experiment_end(self, logs):
elapsed_time = logs["elapsed time"]
self.logger.notice("Finish experiment simulation, took {}".format(elapsed_time))

def on_experiment_error(self, logs):
pass

def on_experiment_infeasible(self, logs):
termination = logs["termination"]
cycle_num = logs["cycle number"][0]
step_num = logs["step number"][0]
operating_conditions = logs["step operating conditions"]
if step_num == 1:
cycle_num -= 1
up_to_step = ""
else:
up_to_step = f", up to step {step_num-1}"
self.logger.warning(
f"\n\n\tExperiment is infeasible: '{termination}' was "
f"triggered during '{operating_conditions}'. The returned solution only "
f"contains the first {cycle_num} cycles{up_to_step}. "
"Try reducing the current, shortening the time interval, or reducing the "
"period.\n\n"
)
Loading