Source code for pybamm.experiment.step.steps

import pybamm

from .base_step import (
    BaseStepExplicit,
    BaseStepImplicit,
    _convert_electric,
    _examples,
)


[docs] def string(text, **kwargs): """ Create a step from a string. Parameters ---------- text : str The string to parse. Each operating condition should be of the form "Do this for this long" or "Do this until this happens". For example, "Charge at 1 C for 1 hour", or "Charge at 1 C until 4.2 V", or "Charge at 1 C for 1 hour or until 4.2 V". The instructions can be of the form "(Dis)charge at x A/C/W", "Rest", or "Hold at x V until y A". The running time should be a time in seconds, minutes or hours, e.g. "10 seconds", "3 minutes" or "1 hour". The stopping conditions should be a circuit state, e.g. "1 A", "C/50" or "3 V". **kwargs Any other keyword arguments are passed to the step class Returns ------- :class:`pybamm.step.BaseStep` A step parsed from the string. """ if not isinstance(text, str): raise TypeError("Input to step.string() must be a string") if "oC" in text: raise ValueError( "Temperature must be specified as a keyword argument " "instead of in the string" ) # Save the original string description = text # extract period if "period)" in text: if "period" in kwargs: raise ValueError( "Period cannot be specified both as a keyword argument " "and in the string" ) text, period_full = text.split(" (") period, _ = period_full.split(" period)") kwargs["period"] = period # extract termination condition based on "until" keyword if "until" in text: # e.g. "Charge at 4 A until 3.8 V" text, termination = text.split(" until ") # sometimes we use "or until" instead of "until", so remove "or" text = text.replace(" or", "") else: termination = None # extract duration based on "for" keyword if "for" in text: # e.g. "Charge at 4 A for 3 hours" text, duration = text.split(" for ") else: duration = None if termination is None and duration is None: raise ValueError( "Operating conditions must contain keyword 'for' or 'until'. " f"For example: {_examples}" ) # read remaining instruction if text.startswith("Rest"): step_class = Rest value = 0 elif text.startswith("Run"): raise ValueError( "Simulating drive cycles with 'Run' has been deprecated. Use the " "pybamm.step.current/voltage/power/c_rate/resistance() functions " "instead." ) else: # split by what is before and after "at" # e.g. "Charge at 4 A" -> ["Charge", "4 A"] # e.g. "Discharge at C/2" -> ["Discharge", "C/2"] instruction, value_string = text.split(" at ") if instruction == "Charge": sign = -1 elif instruction in ["Discharge", "Hold"]: sign = 1 else: raise ValueError( "Instruction must be 'discharge', 'charge', 'rest', or 'hold'. " f"For example: {_examples}" f"The following instruction does not comply: {instruction}" ) # extract units (type) and convert value to float typ, value = _convert_electric(value_string) # Make current positive for discharge and negative for charge value *= sign # Use the appropriate step class step_class = { "current": Current, "voltage": Voltage, "power": Power, "C-rate": CRate, "resistance": Resistance, }[typ] return step_class( value, duration=duration, termination=termination, description=description, **kwargs, )
class Current(BaseStepExplicit): """ Current-controlled step, see :class:`pybamm.step.BaseStep` for arguments. Current is positive for discharge and negative for charge. """ def __init__(self, value, **kwargs): self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def current_value(self, variables): return self.value
[docs] def current(value, **kwargs): """ Current-controlled step, see :class:`pybamm.step.Current`. """ return Current(value, **kwargs)
class Rest(Current): """ Rest step, implemented as a zero-current explicit step. """ def __init__(self, value=0, **kwargs): if value != 0: raise ValueError("Rest steps must have a current value of 0") super().__init__(0, **kwargs) class CRate(BaseStepExplicit): """ C-rate-controlled step, see :class:`pybamm.step.BaseStep` for arguments. C-rate is positive for discharge and negative for charge. """ def __init__(self, value, **kwargs): self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def current_value(self, variables): return self.value * pybamm.Parameter("Nominal cell capacity [A.h]") def _default_timespan(self, value): # "value" is C-rate, so duration is "1 / value" hours in seconds # with a 2x safety factor return 1 / abs(value) * 3600 * 2 def c_rate(value, **kwargs): """ C-rate-controlled step, see :class:`pybamm.step.CRate`. """ return CRate(value, **kwargs) class Voltage(BaseStepImplicit): """ Voltage-controlled step, see :class:`pybamm.step.BaseStep` for arguments. Voltage should always be positive. """ def get_parameter_values(self, variables): return {"Voltage function [V]": self.value} def get_control_residual(self, variables): return variables["Voltage [V]"] - self.value def get_submodel(self, model): return pybamm.external_circuit.VoltageFunctionControl( model.param, model.options )
[docs] def voltage(*args, **kwargs): """ Voltage-controlled step, see :class:`pybamm.step.Voltage`. """ return Voltage(*args, **kwargs)
class Power(BaseStepImplicit): """ Power-controlled step. Power is positive for discharge and negative for charge. Parameters ---------- value : float The value of the power function [W]. **kwargs Any other keyword arguments are passed to the step class """ def __init__(self, value, **kwargs): self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def get_parameter_values(self, variables): return {"Power function [W]": self.value} def get_control_residual(self, variables): return variables["Power [W]"] - self.value def get_submodel(self, model): return pybamm.external_circuit.PowerFunctionControl(model.param, model.options)
[docs] def power(value, **kwargs): """ Power-controlled step, see :class:`pybamm.step.Power`. """ return Power(value, **kwargs)
class Resistance(BaseStepImplicit): """ Resistance-controlled step. Resistance is positive for discharge and negative for charge. Parameters ---------- value : float The value of the power function [W]. **kwargs Any other keyword arguments are passed to the step class """ def __init__(self, value, **kwargs): self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def get_parameter_values(self, variables): return {"Resistance function [Ohm]": self.value} def get_control_residual(self, variables): return variables["Voltage [V]"] - self.value * variables["Current [A]"] def get_submodel(self, model): return pybamm.external_circuit.ResistanceFunctionControl( model.param, model.options )
[docs] def resistance(value, **kwargs): """ Resistance-controlled step, see :class:`pybamm.step.Resistance`. """ return Resistance(value, **kwargs)
def rest(duration=None, **kwargs): """ Create a rest step (see :class:`pybamm.step.Rest`). """ return Rest(duration=duration, **kwargs)
[docs] class CustomStepExplicit(BaseStepExplicit): """ Custom step class where the current value is explicitly given as a function of other variables. When using this class, the user must be careful not to create an expression that depends on the current itself, as this will lead to a circular dependency. For example, in some models, the voltage is an explicit function of the current, so the user should not create a step that depends on the voltage. An expression that works for one model may not work for another. Parameters ---------- current_value_function : callable A function that takes in a dictionary of variables and returns the current value. duration : float, optional The duration of the step in seconds. termination : str or list, optional A string or list of strings indicating the condition(s) that will terminate the step. If a list, the step will terminate when any of the conditions are met. period : float or string, optional The period of the step. If a float, the value is in seconds. If a string, the value should be a valid time string, e.g. "1 hour". temperature : float or string, optional The temperature of the step. If a float, the value is in Kelvin. If a string, the value should be a valid temperature string, e.g. "25 oC". tags : str or list, optional A string or list of strings indicating the tags associated with the step. start_time : str or datetime, optional The start time of the step. description : str, optional A description of the step. direction : str, optional The direction of the step, e.g. "Charge" or "Discharge" or "Rest". Examples -------- Control the current to always be equal to a target power divided by voltage (this is one way to implement a power control step): >>> def current_function(variables): ... P = 4 ... V = variables["Voltage [V]"] ... return P / V Create the step with a 2.5 V termination condition: >>> step = pybamm.step.CustomStepExplicit(current_function, termination="2.5V") """ def __init__(self, current_value_function, **kwargs): super().__init__(None, **kwargs) self.current_value_function = current_value_function self.kwargs = kwargs def current_value(self, variables): return self.current_value_function(variables)
[docs] def copy(self): return CustomStepExplicit(self.current_value_function, **self.kwargs)
[docs] class CustomStepImplicit(BaseStepImplicit): """ Custom step, see :class:`pybamm.step.BaseStep` for arguments. Parameters ---------- current_rhs_function : callable A function that takes in a dictionary of variables and returns the equation controlling the current. control : str, optional Whether the control is algebraic or differential. Default is algebraic, in which case the equation is .. math:: 0 = f(\\text{{variables}}) where :math:`f` is the current_rhs_function. If control is "differential", the equation is .. math:: \\frac{dI}{dt} = f(\\text{{variables}}) duration : float, optional The duration of the step in seconds. termination : str or list, optional A string or list of strings indicating the condition(s) that will terminate the step. If a list, the step will terminate when any of the conditions are met. period : float or string, optional The period of the step. If a float, the value is in seconds. If a string, the value should be a valid time string, e.g. "1 hour". temperature : float or string, optional The temperature of the step. If a float, the value is in Kelvin. If a string, the value should be a valid temperature string, e.g. "25 oC". tags : str or list, optional A string or list of strings indicating the tags associated with the step. start_time : str or datetime, optional The start time of the step. description : str, optional A description of the step. direction : str, optional The direction of the step, e.g. "Charge" or "Discharge" or "Rest". Examples -------- Control the current so that the voltage is constant (without using the built-in voltage control): >>> def voltage_control(variables): ... V = variables["Voltage [V]"] ... return V - 4.2 Create the step with a duration of 1h. In this case we don't need to specify that the control is algebraic, as this is the default. >>> step = pybamm.step.CustomStepImplicit(voltage_control, duration=3600) Alternatively, control the current by a differential equation to achieve a target power: >>> def power_control(variables): ... V = variables["Voltage [V]"] ... # Large time constant to avoid large overshoot. The user should be careful ... # to choose a time constant that is appropriate for the model being used, ... # as well as choosing the appropriate sign for the time constant. ... K_V = 100 ... return K_V * (V - 4.2) Create the step with a 2.5 V termination condition. Now we need to specify that the control is differential. >>> step = pybamm.step.CustomStepImplicit( ... power_control, termination="2.5V", control="differential" ... ) """ def __init__(self, current_rhs_function, control="algebraic", **kwargs): super().__init__(None, **kwargs) self.current_rhs_function = current_rhs_function if control not in ["algebraic", "differential"]: raise ValueError("control must be either 'algebraic' or 'differential'") self.control = control self.kwargs = kwargs def get_control_residual(self, variables): if self.control != "algebraic": raise NotImplementedError( "Unified experiment control only supports algebraic implicit custom steps" ) return self.current_rhs_function(variables) def get_submodel(self, model): return pybamm.external_circuit.FunctionControl( model.param, self.current_rhs_function, model.options, control=self.control )
[docs] def copy(self): return CustomStepImplicit( self.current_rhs_function, self.control, **self.kwargs )