def test_symbol_replacements(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") c = pybamm.Parameter("c") d = pybamm.Parameter("d") replacer = pybamm.SymbolReplacer({a: b, c: d}) for symbol_in, symbol_out in [ (a, b), # just the symbol (a + a, b + b), # binary operator (2 * pybamm.sin(a), 2 * pybamm.sin(b)), # function (3 * b, 3 * b), # no replacement (a + c, b + d), # two replacements ]: replaced_symbol = replacer.process_symbol(symbol_in) self.assertEqual(replaced_symbol.id, symbol_out.id) var1 = pybamm.Variable("var 1", domain="dom 1") var2 = pybamm.Variable("var 2", domain="dom 2") var3 = pybamm.Variable("var 3", domain="dom 1") conc = pybamm.concatenation(var1, var2) replacer = pybamm.SymbolReplacer({var1: var3}) replaced_symbol = replacer.process_symbol(conc) self.assertEqual(replaced_symbol.id, pybamm.concatenation(var3, var2).id)
def new_copy(self): """ Creates an identical copy of the model, using the functionality of :class:`pybamm.SymbolReplacer` but without performing any replacements """ replacer = pybamm.SymbolReplacer({}) return replacer.process_model(self, inplace=False)
def set_up_model_for_experiment_new(self, model): """ Set up self.model to be able to run the experiment (new version). In this version, a new model is created for each step. This increases set-up time since several models to be processed, but reduces simulation time since the model formulation is efficient. """ self.op_conds_to_model_and_param = {} self.op_conds_to_built_models = None for op_cond, op_inputs in zip(self.experiment.operating_conditions, self._experiment_inputs): # Create model for this operating condition if it has not already been seen # before if op_cond[:2] not in self.op_conds_to_model_and_param: if op_inputs["Current switch"] == 1: # Current control # Make a new copy of the model (we will update events later)) new_model = model.new_copy() else: # Voltage or power control # Create a new model where the current density is now a variable # To do so, we replace all instances of the current density in the # model with a current density variable, which is obtained from the # FunctionControl submodel # create the FunctionControl submodel and extract variables external_circuit_variables = ( pybamm.external_circuit.FunctionControl( model.param, None).get_fundamental_variables()) # Perform the replacement symbol_replacement_map = { model.variables[name]: variable for name, variable in external_circuit_variables.items() } replacer = pybamm.SymbolReplacer(symbol_replacement_map) new_model = replacer.process_model(model, inplace=False) # Update the algebraic equation and initial conditions for # FunctionControl # This creates an algebraic equation for the current to allow # current, voltage, or power control, together with the appropriate # guess for the initial condition. # External circuit submodels are always equations on the current # The external circuit function should fix either the current, or # the voltage, or a combination (e.g. I*V for power control) i_cell = new_model.variables["Total current density"] new_model.initial_conditions[ i_cell] = new_model.param.current_with_time # add current events to the model # current events both negative and positive to catch specification new_model.events.extend([ pybamm.Event( "Current cut-off (positive) [A] [experiment]", new_model.variables["Current [A]"] - abs(pybamm.InputParameter("Current cut-off [A]")), ), pybamm.Event( "Current cut-off (negative) [A] [experiment]", new_model.variables["Current [A]"] + abs(pybamm.InputParameter("Current cut-off [A]")), ), ]) if op_inputs["Voltage switch"] == 1: new_model.algebraic[i_cell] = constant_voltage( new_model.variables, pybamm.Parameter("Voltage function [V]"), ) elif op_inputs["Power switch"] == 1: new_model.algebraic[i_cell] = constant_power( new_model.variables, pybamm.Parameter("Power function [W]"), ) # add voltage events to the model if op_inputs["Power switch"] == 1 or op_inputs[ "Current switch"] == 1: new_model.events.append( pybamm.Event( "Voltage cut-off [V] [experiment]", new_model.variables["Terminal voltage [V]"] - op_inputs["Voltage cut-off [V]"] / model.param.n_cells, )) # Keep the min and max voltages as safeguards but add some tolerances # so that they are not triggered before the voltage limits in the # experiment for event in new_model.events: if event.name == "Minimum voltage": event._expression += 1 elif event.name == "Maximum voltage": event._expression -= 1 # Update parameter values new_parameter_values = self.parameter_values.copy() if op_inputs["Current switch"] == 1: new_parameter_values.update({ "Current function [A]": op_inputs["Current input [A]"] }) elif op_inputs["Voltage switch"] == 1: new_parameter_values.update( { "Voltage function [V]": op_inputs["Voltage input [V]"] }, check_already_exists=False, ) elif op_inputs["Power switch"] == 1: new_parameter_values.update( {"Power function [W]": op_inputs["Power input [W]"]}, check_already_exists=False, ) self.op_conds_to_model_and_param[op_cond[:2]] = ( new_model, new_parameter_values, ) self.model = model
def set_up_model_for_experiment_old(self, model): """ Set up self.model to be able to run the experiment (old version). In this version, a single model is created which can then be called with different inputs for current-control, voltage-control, or power-control. This reduces set-up time since only one model needs to be processed, but increases simulation time since the model formulation is inefficient """ # Create a new model where the current density is now a variable # To do so, we replace all instances of the current density in the # model with a current density variable, which is obtained from the # FunctionControl submodel # create the FunctionControl submodel and extract variables external_circuit_variables = pybamm.external_circuit.FunctionControl( model.param, None).get_fundamental_variables() # Perform the replacement symbol_replacement_map = { model.variables[name]: variable for name, variable in external_circuit_variables.items() } replacer = pybamm.SymbolReplacer(symbol_replacement_map) new_model = replacer.process_model(model, inplace=False) # Update the algebraic equation and initial conditions for FunctionControl # This creates an algebraic equation for the current to allow current, voltage, # or power control, together with the appropriate guess for the # initial condition. # External circuit submodels are always equations on the current # The external circuit function should fix either the current, or the voltage, # or a combination (e.g. I*V for power control) i_cell = new_model.variables["Total current density"] new_model.initial_conditions[ i_cell] = new_model.param.current_with_time new_model.algebraic[ i_cell] = constant_current_constant_voltage_constant_power( new_model.variables) # Remove upper and lower voltage cut-offs that are *not* part of the experiment new_model.events = [ event for event in model.events if event.name not in ["Minimum voltage", "Maximum voltage"] ] # add current and voltage events to the model # current events both negative and positive to catch specification new_model.events.extend([ pybamm.Event( "Current cut-off (positive) [A] [experiment]", new_model.variables["Current [A]"] - abs(pybamm.InputParameter("Current cut-off [A]")), ), pybamm.Event( "Current cut-off (negative) [A] [experiment]", new_model.variables["Current [A]"] + abs(pybamm.InputParameter("Current cut-off [A]")), ), pybamm.Event( "Voltage cut-off [V] [experiment]", new_model.variables["Terminal voltage [V]"] - pybamm.InputParameter("Voltage cut-off [V]") / model.param.n_cells, ), ]) self.model = new_model self.op_conds_to_model_and_param = { op_cond[:2]: (new_model, self.parameter_values) for op_cond in set(self.experiment.operating_conditions) } self.op_conds_to_built_models = None
def test_process_model(self): model = pybamm.BaseModel() a = pybamm.Parameter("a") b = pybamm.Parameter("b") c = pybamm.Parameter("c") d = pybamm.Parameter("d") var1 = pybamm.Variable("var1", domain="test") var2 = pybamm.Variable("var2", domain="test") model.rhs = {var1: a * pybamm.grad(var1)} model.algebraic = {var2: c * var2} model.initial_conditions = {var1: b, var2: d} model.boundary_conditions = { var1: { "left": (c, "Dirichlet"), "right": (d, "Neumann") } } model.variables = { "var1": var1, "var2": var2, "grad_var1": pybamm.grad(var1), "d_var1": d * var1, } model.timescale = b model.length_scales = {"test": c} replacer = pybamm.SymbolReplacer({ pybamm.Parameter("a"): pybamm.Scalar(4), pybamm.Parameter("b"): pybamm.Scalar(2), pybamm.Parameter("c"): pybamm.Scalar(3), pybamm.Parameter("d"): pybamm.Scalar(42), }) replacer.process_model(model) # rhs var1 = model.variables["var1"] self.assertIsInstance(model.rhs[var1], pybamm.Multiplication) self.assertIsInstance(model.rhs[var1].children[0], pybamm.Scalar) self.assertIsInstance(model.rhs[var1].children[1], pybamm.Gradient) self.assertEqual(model.rhs[var1].children[0].value, 4) # algebraic var2 = model.variables["var2"] self.assertIsInstance(model.algebraic[var2], pybamm.Multiplication) self.assertIsInstance(model.algebraic[var2].children[0], pybamm.Scalar) self.assertIsInstance(model.algebraic[var2].children[1], pybamm.Variable) self.assertEqual(model.algebraic[var2].children[0].value, 3) # initial conditions self.assertIsInstance(model.initial_conditions[var1], pybamm.Scalar) self.assertEqual(model.initial_conditions[var1].value, 2) # boundary conditions bc_key = list(model.boundary_conditions.keys())[0] self.assertIsInstance(bc_key, pybamm.Variable) bc_value = list(model.boundary_conditions.values())[0] self.assertIsInstance(bc_value["left"][0], pybamm.Scalar) self.assertEqual(bc_value["left"][0].value, 3) self.assertIsInstance(bc_value["right"][0], pybamm.Scalar) self.assertEqual(bc_value["right"][0].value, 42) # variables self.assertEqual(model.variables["var1"].id, var1.id) self.assertIsInstance(model.variables["grad_var1"], pybamm.Gradient) self.assertTrue( isinstance(model.variables["grad_var1"].children[0], pybamm.Variable)) self.assertEqual(model.variables["d_var1"].id, (pybamm.Scalar(42) * var1).id) self.assertIsInstance(model.variables["d_var1"].children[0], pybamm.Scalar) self.assertTrue( isinstance(model.variables["d_var1"].children[1], pybamm.Variable)) # timescale and length scales self.assertEqual(model.timescale.evaluate(), 2) self.assertEqual(model.length_scales["test"].evaluate(), 3)