def __init__( self, t, y, t_event=None, y_event=None, termination="final time", copy_this=None ): self._t = t if isinstance(y, casadi.DM): y = y.full() self._y = y self._t_event = t_event self._y_event = y_event self._termination = termination if copy_this is None: # initialize empty inputs and model, to be populated later self._inputs = pybamm.FuzzyDict() self.model = pybamm.BaseModel() self.set_up_time = None self.solve_time = None self.integration_time = None self.has_symbolic_inputs = False else: self._inputs = copy.copy(copy_this.inputs) self.model = copy_this.model self.set_up_time = copy_this.set_up_time self.solve_time = copy_this.solve_time self.integration_time = copy_this.integration_time self.has_symbolic_inputs = copy_this.has_symbolic_inputs # initiaize empty variables and data self._variables = pybamm.FuzzyDict() self.data = pybamm.FuzzyDict() # initialize empty known evals self._known_evals = defaultdict(dict) for time in t: self._known_evals[time] = {}
def __init__( self, t, y, t_event=None, y_event=None, termination="final time", copy_this=None, ): self._t = t self._y = y self._t_event = t_event self._y_event = y_event self._termination = termination if copy_this is None: # initialize empty inputs and model, to be populated later self._inputs = pybamm.FuzzyDict() self._model = None self.set_up_time = None self.solve_time = None else: self._inputs = copy.copy(copy_this.inputs) self._model = copy_this.model self.set_up_time = copy_this.set_up_time self.solve_time = copy_this.solve_time # initiaize empty variables and data self._variables = pybamm.FuzzyDict() self.data = pybamm.FuzzyDict() # initialize empty known evals self._known_evals = defaultdict(dict) for time in t: self._known_evals[time] = {}
def __init__(self, values=None, chemistry=None): self._dict_items = pybamm.FuzzyDict() # Must provide either values or chemistry, not both (nor neither) if values is not None and chemistry is not None: raise ValueError( "Only one of values and chemistry can be provided. To change parameters" " slightly from a chemistry, first load parameters with the chemistry" " (param = pybamm.ParameterValues(chemistry=...)) and then update with" " param.update({dict of values}).") if values is None and chemistry is None: raise ValueError("values and chemistry cannot both be None") # First load chemistry if chemistry is not None: self.update_from_chemistry(chemistry) # Then update with values dictionary or file if values is not None: # If base_parameters is a filename, load from that filename if isinstance(values, str): file_path = self.find_parameter(values) path = os.path.split(file_path)[0] values = self.read_parameters_csv(file_path) else: path = "" # Don't check parameter already exists when first creating it self.update(values, check_already_exists=False, path=path) # Initialise empty _processed_symbols dict (for caching) self._processed_symbols = {} self.parameter_events = []
def __init__(self, name="Unnamed model"): self.name = name self.options = {} # Initialise empty model self._rhs = {} self._algebraic = {} self._initial_conditions = {} self._boundary_conditions = {} self._variables = pybamm.FuzzyDict() self._events = [] self._concatenated_rhs = None self._concatenated_algebraic = None self._concatenated_initial_conditions = None self._mass_matrix = None self._mass_matrix_inv = None self._jacobian = None self._jacobian_algebraic = None self.external_variables = [] # Default behaviour is to use the jacobian and simplify self.use_jacobian = True self.use_simplify = True self.convert_to_format = "casadi" # Default timescale is 1 second self.timescale = pybamm.Scalar(1)
def build_coupled_variables(self): # Note: pybamm will try to get the coupled variables for the submodels in the # order they are set by the user. If this fails for a particular submodel, # return to it later and try again. If setting coupled variables fails and # there are no more submodels to try, raise an error. submodels = list(self.submodels.keys()) count = 0 # For this part the FuzzyDict of variables is briefly converted back into a # normal dictionary for speed with KeyErrors self._variables = dict(self._variables) while len(submodels) > 0: count += 1 for submodel_name, submodel in self.submodels.items(): if submodel_name in submodels: pybamm.logger.debug( "Getting coupled variables for {} submodel ({})". format(submodel_name, self.name)) try: self.variables.update( submodel.get_coupled_variables(self.variables)) submodels.remove(submodel_name) except KeyError as key: if len(submodels) == 1 or count == 100: # no more submodels to try raise pybamm.ModelError( """Submodel "{}" requires the variable {}, but it cannot be found. Check the selected submodels provide all of the required variables.""".format(submodel_name, key)) else: # try setting coupled variables on next loop through pybamm.logger.debug( "Can't find {}, trying other submodels first". format(key)) # Convert variables back into FuzzyDict self._variables = pybamm.FuzzyDict(self._variables)
def __init__(self, name="Unnamed model"): self.name = name self.options = {} # Initialise empty model self._rhs = {} self._algebraic = {} self._initial_conditions = {} self._boundary_conditions = {} self._variables = pybamm.FuzzyDict({}) self._events = [] self._concatenated_rhs = None self._concatenated_algebraic = None self._concatenated_initial_conditions = None self._mass_matrix = None self._mass_matrix_inv = None self._jacobian = None self._jacobian_algebraic = None self.external_variables = [] self._parameters = None self._input_parameters = None self._variables_casadi = {} # Default behaviour is to use the jacobian self.use_jacobian = True self.convert_to_format = "casadi" # Model is not initially discretised self.is_discretised = False self.y_slices = None # Default timescale is 1 second self.timescale = pybamm.Scalar(1) self.length_scales = {}
def set_summary_variables(self, all_summary_variables): summary_variables = {var: [] for var in all_summary_variables[0]} for sum_vars in all_summary_variables: for name, value in sum_vars.items(): summary_variables[name].append(value) summary_variables["Cycle number"] = range(1, len(all_summary_variables) + 1) self.all_summary_variables = all_summary_variables self._summary_variables = pybamm.FuzzyDict( {name: np.array(value) for name, value in summary_variables.items()} )
def options(self, extra_options): default_options = {"dimensionality": 1} extra_options = extra_options or {} options = pybamm.FuzzyDict(default_options) # any extra options overwrite the default options for name, opt in extra_options.items(): if name in default_options: options[name] = opt else: raise pybamm.OptionError( "Option '{}' not recognised. Best matches are {}".format( name, options.get_best_matches(name))) if options["dimensionality"] not in [1, 2]: raise pybamm.OptionError( "Dimension of current collectors must be 1 or 2, not {}". format(options["dimensionality"])) self._options = options
def options(self, extra_options): default_options = { "operating mode": "current", "dimensionality": 0, "surface form": False, "convection": False, "side reactions": [], "interfacial surface area": "constant", "current collector": "uniform", "particle": "Fickian diffusion", "particle shape": "spherical", "thermal": "isothermal", "cell geometry": None, "external submodels": [], "sei": None, "sei porosity change": False, "working electrode": None, } # Change the default for cell geometry based on which thermal option is provided extra_options = extra_options or {} thermal_option = extra_options.get( "thermal", None ) # return None if option not given if thermal_option is None or thermal_option in ["isothermal", "lumped"]: default_options["cell geometry"] = "arbitrary" else: default_options["cell geometry"] = "pouch" # The "cell geometry" option will still be overridden by extra_options if # provided # Change the default for SEI film resistance based on which sei option is # provided # extra_options = extra_options or {} sei_option = extra_options.get("sei", None) # return None if option not given if sei_option is None: default_options["sei film resistance"] = None else: default_options["sei film resistance"] = "distributed" # The "sei film resistance" option will still be overridden by extra_options if # provided options = pybamm.FuzzyDict(default_options) # any extra options overwrite the default options for name, opt in extra_options.items(): if name in default_options: options[name] = opt else: raise pybamm.OptionError( "Option '{}' not recognised. Best matches are {}".format( name, options.get_best_matches(name) ) ) # Options that are incompatible with models if isinstance(self, pybamm.lithium_ion.BaseModel): if options["convection"] is not False: raise pybamm.OptionError( "convection not implemented for lithium-ion models" ) if ( options["thermal"] in ["x-lumped", "x-full"] and options["cell geometry"] != "pouch" ): raise pybamm.OptionError( options["thermal"] + " model must have pouch geometry." ) if isinstance(self, pybamm.lead_acid.BaseModel): if options["thermal"] != "isothermal" and options["dimensionality"] != 0: raise pybamm.OptionError( "Lead-acid models can only have thermal " "effects if dimensionality is 0." ) if options["sei"] is not None or options["sei film resistance"] is not None: raise pybamm.OptionError("Lead-acid models cannot have SEI formation") # Some standard checks to make sure options are compatible if not ( options["operating mode"] in ["current", "voltage", "power"] or callable(options["operating mode"]) ): raise pybamm.OptionError( "operating mode '{}' not recognised".format(options["operating mode"]) ) if ( isinstance(self, (pybamm.lead_acid.LOQS, pybamm.lead_acid.Composite)) and options["surface form"] is False ): if len(options["side reactions"]) > 0: raise pybamm.OptionError( """must use surface formulation to solve {!s} with side reactions """.format( self ) ) if options["surface form"] not in [False, "differential", "algebraic"]: raise pybamm.OptionError( "surface form '{}' not recognised".format(options["surface form"]) ) if options["convection"] not in [ False, "uniform transverse", "full transverse", ]: raise pybamm.OptionError( "convection option '{}' not recognised".format(options["convection"]) ) if options["current collector"] not in [ "uniform", "potential pair", "potential pair quite conductive", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( options["current collector"] ) ) if options["dimensionality"] not in [0, 1, 2]: raise pybamm.OptionError( "Dimension of current collectors must be 0, 1, or 2, not {}".format( options["dimensionality"] ) ) if options["thermal"] not in ["isothermal", "lumped", "x-lumped", "x-full"]: raise pybamm.OptionError( "Unknown thermal model '{}'".format(options["thermal"]) ) if options["cell geometry"] not in ["arbitrary", "pouch"]: raise pybamm.OptionError( "Unknown geometry '{}'".format(options["cell geometry"]) ) if options["sei"] not in [ None, "constant", "reaction limited", "solvent-diffusion limited", "electron-migration limited", "interstitial-diffusion limited", "ec reaction limited", ]: raise pybamm.OptionError("Unknown sei model '{}'".format(options["sei"])) if options["sei film resistance"] not in [None, "distributed", "average"]: raise pybamm.OptionError( "Unknown sei film resistance model '{}'".format( options["sei film resistance"] ) ) if options["sei porosity change"] not in [True, False]: raise pybamm.OptionError( "Unknown sei porosity change '{}'".format( options["sei porosity change"] ) ) if options["dimensionality"] == 0: if options["current collector"] not in ["uniform"]: raise pybamm.OptionError( "current collector model must be uniform in 0D model" ) if options["convection"] == "full transverse": raise pybamm.OptionError( "cannot have transverse convection in 0D model" ) if options["particle"] not in [ "Fickian diffusion", "fast diffusion", "uniform profile", "quadratic profile", "quartic profile", ]: raise pybamm.OptionError( "particle model '{}' not recognised".format(options["particle"]) ) if options["particle"] == "fast diffusion": raise NotImplementedError( "The 'fast diffusion' option has been renamed. " "Use 'uniform profile' instead." ) if options["particle shape"] not in ["spherical", "user"]: raise pybamm.OptionError( "particle shape '{}' not recognised".format(options["particle shape"]) ) if options["thermal"] == "x-lumped" and options["dimensionality"] == 1: warnings.warn( "1+1D Thermal models are only valid if both tabs are " "placed at the top of the cell." ) self._options = options
def print_parameters(self, parameters, output_file=None): """ Return dictionary of evaluated parameters, and optionally print these evaluated parameters to an output file. For dimensionless parameters that depend on the C-rate, the value is given as a function of the C-rate (either x * Crate or x / Crate depending on the dependence) Parameters ---------- parameters : class or dict containing :class:`pybamm.Parameter` objects Class or dictionary containing all the parameters to be evaluated output_file : string, optional The file to print parameters to. If None, the parameters are not printed, and this function simply acts as a test that all the parameters can be evaluated, and returns the dictionary of evaluated parameters. Returns ------- evaluated_parameters : defaultdict The evaluated parameters, for further processing if needed Notes ----- A C-rate of 1 C is the current required to fully discharge the battery in 1 hour, 2 C is current to discharge the battery in 0.5 hours, etc """ # Set list of attributes to ignore, for when we are evaluating parameters from # a class of parameters ignore = [ "__name__", "__doc__", "__package__", "__loader__", "__spec__", "__file__", "__cached__", "__builtins__", "absolute_import", "division", "print_function", "unicode_literals", "pybamm", "_options", "constants", "np", "geo", "elec", "therm", ] # If 'parameters' is a class, extract the dict if not isinstance(parameters, dict): parameters = { k: v for k, v in parameters.__dict__.items() if k not in ignore } evaluated_parameters = defaultdict(list) # Calculate parameters for each C-rate for Crate in [1, 10]: # Update Crate capacity = self.get("Nominal cell capacity [A.h]") if capacity is not None: self.update( {"Current function [A]": Crate * capacity}, check_already_exists=False, ) # Turn to regular dictionary for faster KeyErrors self._dict_items = dict(self._dict_items) for name, symbol in parameters.items(): if not callable(symbol): try: proc_symbol = self.process_symbol(symbol) except KeyError: # skip parameters that don't have a value in that parameter set proc_symbol = None if not (callable(proc_symbol) or proc_symbol is None or proc_symbol.has_symbol_of_classes( (pybamm.Concatenation, pybamm.Broadcast))): evaluated_parameters[name].append( proc_symbol.evaluate(t=0)) # Turn back to FuzzyDict self._dict_items = pybamm.FuzzyDict(self._dict_items) # Calculate C-dependence of the parameters based on the difference between the # value at 1C and the value at C / 10 for name, values in evaluated_parameters.items(): if values[1] == 0 or abs(values[0] / values[1] - 1) < 1e-10: C_dependence = "" elif abs(values[0] / values[1] - 10) < 1e-10: C_dependence = " * Crate" elif abs(values[0] / values[1] - 0.1) < 1e-10: C_dependence = " / Crate" evaluated_parameters[name] = (values[0], C_dependence) # Print the evaluated_parameters dict to output_file if output_file: self.print_evaluated_parameters(evaluated_parameters, output_file) return evaluated_parameters
def variables(self, variables): self._variables = pybamm.FuzzyDict(variables)
def test_fuzzy_dict(self): d = pybamm.FuzzyDict({"test": 1, "test2": 2}) self.assertEqual(d["test"], 1) with self.assertRaisesRegex(KeyError, "'test3' not found. Best matches are "): d["test3"]
def __init__(self, extra_options): self.possible_options = { "surface form": ["false", "differential", "algebraic"], "convection": ["none", "uniform transverse", "full transverse"], "current collector": [ "uniform", "potential pair", "potential pair quite conductive", ], "dimensionality": [0, 1, 2], "interfacial surface area": ["constant", "varying"], "thermal": ["isothermal", "lumped", "x-lumped", "x-full"], "cell geometry": ["arbitrary", "pouch"], "SEI": [ "none", "constant", "reaction limited", "solvent-diffusion limited", "electron-migration limited", "interstitial-diffusion limited", "ec reaction limited", ], "SEI film resistance": ["none", "distributed", "average"], "SEI porosity change": ["true", "false"], "lithium plating": ["none", "reversible", "irreversible"], "loss of active material": ["none", "negative", "positive", "both"], "operating mode": ["current", "voltage", "power"], "particle cracking": [ "none", "no cracking", "negative", "positive", "both", ], "lithium plating porosity change": ["true", "false"], "particle": [ "Fickian diffusion", "fast diffusion", "uniform profile", "quadratic profile", "quartic profile", ], "particle shape": ["spherical", "user", "no particles"], "electrolyte conductivity": [ "default", "full", "leading order", "composite", "integrated", ], "total interfacial current density as a state": ["true", "false"], } default_options = { "operating mode": "current", "dimensionality": 0, "surface form": "false", "convection": "none", "side reactions": [], "interfacial surface area": "constant", "current collector": "uniform", "particle": "Fickian diffusion", "particle shape": "spherical", "electrolyte conductivity": "default", "thermal": "isothermal", "cell geometry": "none", "external submodels": [], "SEI": "none", "lithium plating": "none", "SEI porosity change": "false", "lithium plating porosity change": "false", "loss of active material": "none", "working electrode": "none", "particle cracking": "none", "total interfacial current density as a state": "false", } # Change the default for cell geometry based on which thermal option is provided extra_options = extra_options or {} thermal_option = extra_options.get("thermal", "none") # return "none" if option not given if thermal_option in ["none", "isothermal", "lumped"]: default_options["cell geometry"] = "arbitrary" else: default_options["cell geometry"] = "pouch" # The "cell geometry" option will still be overridden by extra_options if # provided # Change the default for SEI film resistance based on which SEI option is # provided # extra_options = extra_options or {} sei_option = extra_options.get("SEI", "none") # return "none" if option not given if sei_option == "none": default_options["SEI film resistance"] = "none" else: default_options["SEI film resistance"] = "distributed" # The "SEI film resistance" option will still be overridden by extra_options if # provided options = pybamm.FuzzyDict(default_options) # any extra options overwrite the default options for name, opt in extra_options.items(): if name in default_options: options[name] = opt else: raise pybamm.OptionError( "Option '{}' not recognised. Best matches are {}".format( name, options.get_best_matches(name))) # If "SEI film resistance" is "distributed" then "total interfacial current # density as a state" must be "true" if options["SEI film resistance"] == "distributed": options["total interfacial current density as a state"] = "true" # Check that extra_options did not try to provide a clashing option if (extra_options.get( "total interfacial current density as a state") == "false" ): raise pybamm.OptionError( "If 'sei film resistance' is 'distributed' then 'total interfacial " "current density as a state' must be 'true'") # Some standard checks to make sure options are compatible if options["SEI porosity change"] in [True, False]: raise pybamm.OptionError( "SEI porosity change must now be given in string format " "('true' or 'false')") if options["dimensionality"] == 0: if options["current collector"] not in ["uniform"]: raise pybamm.OptionError( "current collector model must be uniform in 0D model") if options["convection"] == "full transverse": raise pybamm.OptionError( "cannot have transverse convection in 0D model") if options["particle"] == "fast diffusion": raise NotImplementedError( "The 'fast diffusion' option has been renamed. " "Use 'uniform profile' instead.") if options["thermal"] == "x-lumped" and options["dimensionality"] == 1: warnings.warn( "1+1D Thermal models are only valid if both tabs are " "placed at the top of the cell.") for option, value in options.items(): if (option == "side reactions" or option == "external submodels" or option == "working electrode"): pass elif value not in self.possible_options[option]: if not (option == "operating mode" and callable(value)): raise pybamm.OptionError( f"\n'{value}' is not recognized in option '{option}'. " f"Possible values are {self.possible_options[option]}") super().__init__(options.items())
def __init__( self, all_ts, all_ys, all_models, all_inputs, t_event=None, y_event=None, termination="final time", ): if not isinstance(all_ts, list): all_ts = [all_ts] if not isinstance(all_ys, list): all_ys = [all_ys] if not isinstance(all_models, list): all_models = [all_models] self._all_ts = all_ts self._all_ys = all_ys self._all_models = all_models self._t_event = t_event self._y_event = y_event self._termination = termination # Set up inputs if not isinstance(all_inputs, list): for key, value in all_inputs.items(): if isinstance(value, numbers.Number): all_inputs[key] = np.array([value]) all_inputs = [all_inputs] self.all_inputs = all_inputs self.has_symbolic_inputs = any( isinstance(v, casadi.MX) for v in all_inputs[0].values()) # Copy the timescale_eval and lengthscale_evals if they exist if hasattr(all_models[0], "timescale_eval"): self.timescale_eval = all_models[0].timescale_eval else: self.timescale_eval = all_models[0].timescale.evaluate() if hasattr(all_models[0], "length_scales_eval"): self.length_scales_eval = all_models[0].length_scales_eval else: self.length_scales_eval = { domain: scale.evaluate() for domain, scale in all_models[0].length_scales.items() } self.set_up_time = None self.solve_time = None self.integration_time = None # initiaize empty variables and data self._variables = pybamm.FuzzyDict() self.data = pybamm.FuzzyDict() # Add self as sub-solution for compatibility with ProcessedVariable self._sub_solutions = [self] # Solution now uses CasADi pybamm.citations.register("Andersson2019")
def options(self, extra_options): default_options = { "operating mode": "current", "dimensionality": 0, "surface form": False, "convection": False, "side reactions": [], "interfacial surface area": "constant", "current collector": "uniform", "particle": "Fickian diffusion", "thermal": "isothermal", "thermal current collector": False, "external submodels": [], } options = pybamm.FuzzyDict(default_options) # any extra options overwrite the default options if extra_options is not None: for name, opt in extra_options.items(): if name in default_options: options[name] = opt else: raise pybamm.OptionError( "Option '{}' not recognised. Best matches are {}". format(name, options.get_best_matches(name))) # Some standard checks to make sure options are compatible if not (options["operating mode"] in ["current", "voltage", "power"] or callable(options["operating mode"])): raise pybamm.OptionError( "operating mode '{}' not recognised".format( options["operating mode"])) if (isinstance(self, (pybamm.lead_acid.LOQS, pybamm.lead_acid.Composite)) and options["surface form"] is False): if len(options["side reactions"]) > 0: raise pybamm.OptionError(""" must use surface formulation to solve {!s} with side reactions """.format(self)) if options["surface form"] not in [False, "differential", "algebraic"]: raise pybamm.OptionError("surface form '{}' not recognised".format( options["surface form"])) if options["current collector"] not in [ "uniform", "potential pair", "potential pair quite conductive", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( options["current collector"])) if options["dimensionality"] not in [0, 1, 2]: raise pybamm.OptionError( "Dimension of current collectors must be 0, 1, or 2, not {}". format(options["dimensionality"])) if options["thermal"] not in [ "isothermal", "x-full", "x-lumped", "xyz-lumped", "lumped", ]: raise pybamm.OptionError("Unknown thermal model '{}'".format( options["thermal"])) if options["particle"] not in ["Fickian diffusion", "fast diffusion"]: raise pybamm.OptionError( "particle model '{}' not recognised".format( options["particle"])) # Options that are incompatible with models if isinstance(self, pybamm.lithium_ion.BaseModel): if options["convection"] is True: raise pybamm.OptionError( "convection not implemented for lithium-ion models") if isinstance(self, pybamm.lead_acid.BaseModel): if options["thermal"] != "isothermal" and options[ "dimensionality"] != 0: raise pybamm.OptionError( "Lead-acid models can only have thermal " "effects if dimensionality is 0.") if options["thermal current collector"] is True: raise pybamm.OptionError( "Thermal current collector effects are not implemented " "for lead-acid models.") self._options = options
def get_cycle_summary_variables(cycle_solution, esoh_sim): Q = cycle_solution["Discharge capacity [A.h]"].data min_Q = np.min(Q) max_Q = np.max(Q) cycle_summary_variables = pybamm.FuzzyDict( { "Minimum measured discharge capacity [A.h]": min_Q, "Maximum measured discharge capacity [A.h]": max_Q, "Measured capacity [A.h]": max_Q - min_Q, } ) degradation_variables = [ "Negative electrode capacity [A.h]", "Positive electrode capacity [A.h]", # LAM, LLI "Loss of active material in negative electrode [%]", "Loss of active material in positive electrode [%]", "Loss of lithium inventory [%]", "Loss of lithium inventory, including electrolyte [%]", # Total lithium "Total lithium [mol]", "Total lithium in electrolyte [mol]", "Total lithium in positive electrode [mol]", "Total lithium in negative electrode [mol]", "Total lithium in particles [mol]", # Lithium lost "Total lithium lost [mol]", "Total lithium lost from particles [mol]", "Total lithium lost from electrolyte [mol]", "Loss of lithium to negative electrode SEI [mol]", "Loss of lithium to positive electrode SEI [mol]", "Loss of lithium to negative electrode lithium plating [mol]", "Loss of lithium to positive electrode lithium plating [mol]", "Loss of capacity to negative electrode SEI [A.h]", "Loss of capacity to positive electrode SEI [A.h]", "Loss of capacity to negative electrode lithium plating [A.h]", "Loss of capacity to positive electrode lithium plating [A.h]", "Total lithium lost to side reactions [mol]", "Total capacity lost to side reactions [A.h]", # Resistance "Local ECM resistance [Ohm]", ] first_state = cycle_solution.first_state last_state = cycle_solution.last_state for var in degradation_variables: data_first = first_state[var].data data_last = last_state[var].data cycle_summary_variables[var] = data_last[0] var_lowercase = var[0].lower() + var[1:] cycle_summary_variables["Change in " + var_lowercase] = ( data_last[0] - data_first[0] ) if esoh_sim is not None: V_min = esoh_sim.parameter_values["Lower voltage cut-off [V]"] V_max = esoh_sim.parameter_values["Upper voltage cut-off [V]"] C_n = last_state["Negative electrode capacity [A.h]"].data[0] C_p = last_state["Positive electrode capacity [A.h]"].data[0] n_Li = last_state["Total lithium in particles [mol]"].data[0] if esoh_sim.solution is not None: # initialize with previous solution if it is available esoh_sim.built_model.set_initial_conditions_from(esoh_sim.solution) solver = None else: x_100_init = np.max(cycle_solution["Negative electrode SOC"].data) # make sure x_0 > 0 C_init = np.minimum(0.95 * (C_n * x_100_init), max_Q - min_Q) # Solve the esoh model and add outputs to the summary variables # use CasadiAlgebraicSolver if there are interpolants if isinstance( esoh_sim.parameter_values["Negative electrode OCP [V]"], tuple ) or isinstance( esoh_sim.parameter_values["Positive electrode OCP [V]"], tuple ): solver = pybamm.CasadiAlgebraicSolver() # Choose x_100_init so as not to violate the interpolation limits if isinstance( esoh_sim.parameter_values["Positive electrode OCP [V]"], tuple ): y_100_min = np.min( esoh_sim.parameter_values["Positive electrode OCP [V]"][1][:, 0] ) x_100_max = ( n_Li * pybamm.constants.F.value / 3600 - y_100_min * C_p ) / C_n x_100_init = np.minimum(x_100_init, 0.99 * x_100_max) else: solver = None # Update initial conditions using the cycle solution esoh_sim.build() esoh_sim.built_model.set_initial_conditions_from( {"x_100": x_100_init, "C": C_init} ) esoh_sol = esoh_sim.solve( [0], inputs={ "V_min": V_min, "V_max": V_max, "C_n": C_n, "C_p": C_p, "n_Li": n_Li, }, solver=solver, ) for var in esoh_sim.built_model.variables: cycle_summary_variables[var] = esoh_sol[var].data[0] cycle_summary_variables["Capacity [A.h]"] = cycle_summary_variables["C"] return cycle_summary_variables