From 50b85ab0da0e3622fd0dcc9f63959afe808927bd Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 23 Jul 2019 16:24:17 +0100 Subject: [PATCH 001/122] #548 add 1d curr coll --- .../one_dimensional_current_collector.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 pybamm/models/submodels/current_collector/one_dimensional_current_collector.py diff --git a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py new file mode 100644 index 0000000000..fc84e03703 --- /dev/null +++ b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py @@ -0,0 +1,82 @@ +# +# Class for one-dimensional current collectors +# +import pybamm +from .base_current_collector import BaseModel + + +class OneDimensionalCurrentCollector(BaseModel): + """A submodel 1D current collectors. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + + + **Extends:** :class:`pybamm.current_collector.BaseModel` + """ + + def __init__(self, param): + super().__init__(param) + + def get_fundamental_variables(self): + + phi_s_cn = pybamm.standard_variables.phi_s_cn + phi_s_cp = pybamm.standard_variables.phi_s_cp + + variables = self._get_standard_potential_variables(phi_s_cn, phi_s_cp) + + # TO DO: grad not implemented for 2D yet + i_cc = pybamm.Scalar(0) + i_boundary_cc = pybamm.standard_variables.i_boundary_cc + + variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) + + return variables + + def set_algebraic(self, variables): + + ocp_p_av = variables["Average positive electrode open circuit potential"] + ocp_n_av = variables["Average negative electrode open circuit potential"] + eta_r_n_av = variables["Average negative electrode reaction overpotential"] + eta_r_p_av = variables["Average positive electrode reaction overpotential"] + eta_e_av = variables["Average electrolyte overpotential"] + delta_phi_s_n_av = variables["Average negative electrode ohmic losses"] + delta_phi_s_p_av = variables["Average positive electrode ohmic losses"] + + i_boundary_cc = variables["Current collector current density"] + v_boundary_cc = variables["Local current collector potential difference"] + + # The voltage-current expression from the SPM(e) + local_voltage_expression = ( + ocp_p_av + - ocp_n_av + + eta_r_p_av + - eta_r_n_av + + eta_e_av + + delta_phi_s_p_av + - delta_phi_s_n_av + ) + + self.algebraic = {i_boundary_cc: v_boundary_cc - local_voltage_expression} + + def set_rhs(self, variables): + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] + self.rhs = {phi_s_cn: 0, phi_s_cp: 0} + + def set_initial_conditions(self, variables): + + param = self.param + applied_current = param.current_with_time + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] + i_boundary_cc = variables["Current collector current density"] + + self.initial_conditions = { + phi_s_cn: pybamm.Scalar(0), + phi_s_cp: param.U_p(param.c_p_init, param.T_ref) + - param.U_n(param.c_n_init, param.T_ref), + i_boundary_cc: applied_current / param.l_y / param.l_z, + } From a0605b1d62089e5d1bf4d72d85e3da224b0c05aa Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 23 Jul 2019 17:00:44 +0100 Subject: [PATCH 002/122] #548 basic 1d current collector submodel --- .../full_battery_models/base_battery_model.py | 2 +- .../full_battery_models/lithium_ion/spm.py | 6 +-- .../submodels/current_collector/__init__.py | 1 + .../one_dimensional_current_collector.py | 40 ++++++---------- results/2plus1D/spm-1plus1D.py | 47 +++++++++++++++++++ 5 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 results/2plus1D/spm-1plus1D.py diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 2881dd3ff0..a23d4de1d2 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -457,7 +457,7 @@ def set_voltage_variables(self): # Cut-off voltage voltage = self.variables["Terminal voltage"] - self.events["Minimum voltage"] = voltage - self.param.voltage_low_cut + # self.events["Minimum voltage"] = voltage - self.param.voltage_low_cut def process_parameters_and_discretise(self, symbol): """ diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index 81300045d4..d5503f76b0 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -33,9 +33,9 @@ def set_current_collector_submodel(self): self.param ) elif self.options["bc_options"]["dimensionality"] == 1: - raise NotImplementedError( - "One-dimensional current collector submodel not implemented." - ) + self.submodels[ + "current collector" + ] = pybamm.current_collector.OneDimensionalCurrentCollector(self.param) elif self.options["bc_options"]["dimensionality"] == 2: self.submodels[ "current collector" diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index 3bfc16fcb8..9d0de7ecb8 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -2,3 +2,4 @@ from .homogeneous_current_collector import Uniform from .single_particle_current_collector import SingleParticlePotentialPair +from .one_dimensional_current_collector import OneDimensionalCurrentCollector diff --git a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py index fc84e03703..fd312e2cbf 100644 --- a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py +++ b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py @@ -37,34 +37,22 @@ def get_fundamental_variables(self): def set_algebraic(self, variables): - ocp_p_av = variables["Average positive electrode open circuit potential"] - ocp_n_av = variables["Average negative electrode open circuit potential"] - eta_r_n_av = variables["Average negative electrode reaction overpotential"] - eta_r_p_av = variables["Average positive electrode reaction overpotential"] - eta_e_av = variables["Average electrolyte overpotential"] - delta_phi_s_n_av = variables["Average negative electrode ohmic losses"] - delta_phi_s_p_av = variables["Average positive electrode ohmic losses"] - - i_boundary_cc = variables["Current collector current density"] - v_boundary_cc = variables["Local current collector potential difference"] - - # The voltage-current expression from the SPM(e) - local_voltage_expression = ( - ocp_p_av - - ocp_n_av - + eta_r_p_av - - eta_r_n_av - + eta_e_av - + delta_phi_s_p_av - - delta_phi_s_n_av - ) - - self.algebraic = {i_boundary_cc: v_boundary_cc - local_voltage_expression} - - def set_rhs(self, variables): phi_s_cn = variables["Negative current collector potential"] phi_s_cp = variables["Positive current collector potential"] - self.rhs = {phi_s_cn: 0, phi_s_cp: 0} + i_boundary_cc = variables["Current collector current density"] + + param = self.param + applied_current = param.current_with_time + + self.algebraic = { + phi_s_cn: phi_s_cn, + phi_s_cp: phi_s_cp + - ( + param.U_p(param.c_p_init, param.T_ref) + - param.U_n(param.c_n_init, param.T_ref) + ), + i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z, + } def set_initial_conditions(self, variables): diff --git a/results/2plus1D/spm-1plus1D.py b/results/2plus1D/spm-1plus1D.py new file mode 100644 index 0000000000..64a64d7de5 --- /dev/null +++ b/results/2plus1D/spm-1plus1D.py @@ -0,0 +1,47 @@ +import pybamm +import numpy as np +import sys + +# set logging level +pybamm.set_logging_level("INFO") + +# load (2+1D) SPM model +options = {"bc_options": {"dimensionality": 1}} +model = pybamm.lithium_ion.SPM(options) +model.check_well_posedness() + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry +param = model.default_parameter_values +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +var = pybamm.standard_spatial_vars +var_pts = { + var.x_n: 10, + var.x_s: 10, + var.x_p: 10, + var.r_n: 10, + var.r_p: 10, + var.y: 10, + var.z: 10, +} +# depnding on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, model.default_spatial_methods) +disc.process_model(model) + +# solve model -- simulate one hour discharge +tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) +t_end = 3600 / tau.evaluate(0) +t_eval = np.linspace(0, t_end, 120) +solution = model.default_solver.solve(model, t_eval) +import ipdb + +ipdb.set_trace() From 6d1c6acfe868601818097398c0d8df661c4ff711 Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 25 Jul 2019 15:31:23 +0100 Subject: [PATCH 003/122] playing with 1d_cc --- .../one_dimensional_current_collector.py | 80 +++++++++++++++++-- results/2plus1D/spm-1plus1D.py | 46 +++++++++-- results/2plus1D/spm-2plus1D.py | 8 ++ 3 files changed, 119 insertions(+), 15 deletions(-) diff --git a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py index fd312e2cbf..b75bbabda5 100644 --- a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py +++ b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py @@ -35,25 +35,89 @@ def get_fundamental_variables(self): return variables +# def set_algebraic(self, variables): +# +# phi_s_cn = variables["Negative current collector potential"] +# phi_s_cp = variables["Positive current collector potential"] +# i_boundary_cc = variables["Current collector current density"] +# +# param = self.param +# applied_current = param.current_with_time +# +# self.algebraic = { +# phi_s_cn: phi_s_cn, +# phi_s_cp: phi_s_cp +# - ( +# param.U_p(param.c_p_init, param.T_ref) +# - param.U_n(param.c_n_init, param.T_ref) +# ) - 0.1*param.l_z, +# i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z, +# } + def set_algebraic(self, variables): + param = self.param + + ocp_p_av = variables["Average positive electrode open circuit potential"] + ocp_n_av = variables["Average negative electrode open circuit potential"] + eta_r_n_av = variables["Average negative electrode reaction overpotential"] + eta_r_p_av = variables["Average positive electrode reaction overpotential"] + eta_e_av = variables["Average electrolyte overpotential"] + delta_phi_s_n_av = variables["Average negative electrode ohmic losses"] + delta_phi_s_p_av = variables["Average positive electrode ohmic losses"] + phi_s_cn = variables["Negative current collector potential"] phi_s_cp = variables["Positive current collector potential"] i_boundary_cc = variables["Current collector current density"] + v_boundary_cc = variables["Local current collector potential difference"] + applied_current = param.current_with_time + # The voltage-current expression from the SPM(e) + local_voltage_expression = ( + ocp_p_av + - ocp_n_av + + eta_r_p_av + - eta_r_n_av + + eta_e_av + + delta_phi_s_p_av + - delta_phi_s_n_av + ) + + self.algebraic = { + phi_s_cn: pybamm.div(pybamm.grad((phi_s_cn))) + - (param.sigma_cn * param.delta ** 2 / param.l_cn) + * pybamm.source(i_boundary_cc, phi_s_cn), + phi_s_cp: pybamm.div(pybamm.grad(phi_s_cp)) + + (param.sigma_cp * param.delta ** 2 / param.l_cp) + * pybamm.source(i_boundary_cc, phi_s_cp), +# i_boundary_cc: v_boundary_cc - local_voltage_expression, + i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z - param.z/10, + } + + def set_boundary_conditions(self, variables): + + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] param = self.param applied_current = param.current_with_time - self.algebraic = { - phi_s_cn: phi_s_cn, - phi_s_cp: phi_s_cp - - ( - param.U_p(param.c_p_init, param.T_ref) - - param.U_n(param.c_n_init, param.T_ref) - ), - i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z, + pos_tab_bc = -applied_current / ( + param.sigma_cp * param.delta ** 2 * param.l_tab_p * param.l_cp + ) + + self.boundary_conditions = { + phi_s_cn: { + "left": (pybamm.Scalar(0), "Dirichlet"), + "right": (pybamm.Scalar(0), "Neumann"), + }, + phi_s_cp: { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (pybamm.Scalar(3.0), "Dirichlet"), + }, } + return variables + def set_initial_conditions(self, variables): param = self.param diff --git a/results/2plus1D/spm-1plus1D.py b/results/2plus1D/spm-1plus1D.py index 64a64d7de5..1837f7d241 100644 --- a/results/2plus1D/spm-1plus1D.py +++ b/results/2plus1D/spm-1plus1D.py @@ -1,7 +1,8 @@ import pybamm import numpy as np import sys - +import matplotlib.pyplot as plt +plt.close('all') # set logging level pybamm.set_logging_level("INFO") @@ -22,12 +23,12 @@ var = pybamm.standard_spatial_vars var_pts = { var.x_n: 10, - var.x_s: 10, + var.x_s: 5, var.x_p: 10, var.r_n: 10, var.r_p: 10, - var.y: 10, - var.z: 10, + var.y: 1, + var.z: 5, } # depnding on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) @@ -40,8 +41,39 @@ # solve model -- simulate one hour discharge tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) -t_eval = np.linspace(0, t_end, 120) +t_eval = np.linspace(0, t_end, 10) solution = model.default_solver.solve(model, t_eval) -import ipdb -ipdb.set_trace() +#e_conc = pybamm.ProcessedVariable( +# model.variables['Electrolyte concentration [mol.m-3]'], +# solution.t, +# solution.y, +# mesh=mesh, +# ) + +# plot +#plot = pybamm.QuickPlot(model, mesh, solution) +#plot.dynamic_plot() + +def plot_var(var, time=-1): + variable = model.variables[var] + len_x = len(mesh.combine_submeshes(*variable.domain)) + len_z = variable.shape[0] // len_x + entries = np.empty((len_x, len_z, len(solution.t))) + + for idx in range(len(solution.t)): + t = solution.t[idx] + y = solution.y[:, idx] + entries[:, :, idx] = np.reshape( + variable.evaluate(t, y), [len_x, len_z] + ) + plt.figure() + for bat_id in range(len_x): + plt.plot(range(len_z), entries[bat_id, :, time].flatten()) + plt.figure() + plt.imshow(entries[:, :, time]) + +#plot_var(var="Electrolyte concentration") +plot_var(var="Interfacial current density", time=-1) +#plot_var(var="Current collector current density", time=[0]) +#plot_var(var="Local current collector potential difference", time=[0]) diff --git a/results/2plus1D/spm-2plus1D.py b/results/2plus1D/spm-2plus1D.py index e269abff57..10c1de6293 100644 --- a/results/2plus1D/spm-2plus1D.py +++ b/results/2plus1D/spm-2plus1D.py @@ -57,6 +57,14 @@ solution.y, mesh=mesh, ) + +e_conc = pybamm.ProcessedVariable( + model.variables['Electrolyte concentration [mol.m-3]'], + solution.t, + solution.y, + mesh=mesh, +) + l_y = phi_s_cp.x_sol[-1] l_z = phi_s_cp.z_sol[-1] y_plot = np.linspace(0, l_y, 21) From b0f472371824ed365496981b955f6f5999fb374b Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 25 Jul 2019 15:35:35 +0100 Subject: [PATCH 004/122] undo change to 2plus1d --- .../one_dimensional_current_collector.py | 2 +- results/2plus1D/spm-2plus1D.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py index b75bbabda5..e584ce9dbb 100644 --- a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py +++ b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py @@ -90,7 +90,7 @@ def set_algebraic(self, variables): + (param.sigma_cp * param.delta ** 2 / param.l_cp) * pybamm.source(i_boundary_cc, phi_s_cp), # i_boundary_cc: v_boundary_cc - local_voltage_expression, - i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z - param.z/10, + i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z, } def set_boundary_conditions(self, variables): diff --git a/results/2plus1D/spm-2plus1D.py b/results/2plus1D/spm-2plus1D.py index 10c1de6293..e269abff57 100644 --- a/results/2plus1D/spm-2plus1D.py +++ b/results/2plus1D/spm-2plus1D.py @@ -57,14 +57,6 @@ solution.y, mesh=mesh, ) - -e_conc = pybamm.ProcessedVariable( - model.variables['Electrolyte concentration [mol.m-3]'], - solution.t, - solution.y, - mesh=mesh, -) - l_y = phi_s_cp.x_sol[-1] l_z = phi_s_cp.z_sol[-1] y_plot = np.linspace(0, l_y, 21) From 8ff71d3ad85ec01dba4294e4baa9942c8be1e2fd Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 5 Aug 2019 16:27:56 +0100 Subject: [PATCH 005/122] #548 add script to update statevector based on variable name --- .../full_battery_models/base_battery_model.py | 6 + .../full_battery_models/lithium_ion/spm.py | 8 +- .../submodels/current_collector/__init__.py | 1 + .../one_dimensional_current_collector.py | 134 --------------- .../set_potential_single_particle.py | 152 ++++++++++++++++++ .../submodels/electrode/base_electrode.py | 32 ++-- .../submodels/electrode/ohm/base_ohm.py | 4 +- .../submodels/electrode/ohm/leading_ohm.py | 4 +- results/2plus1D/set-potential-spm-1plus1D.py | 97 +++++++++++ results/2plus1D/spm-1plus1D.py | 25 +-- 10 files changed, 299 insertions(+), 164 deletions(-) delete mode 100644 pybamm/models/submodels/current_collector/one_dimensional_current_collector.py create mode 100644 pybamm/models/submodels/current_collector/set_potential_single_particle.py create mode 100644 results/2plus1D/set-potential-spm-1plus1D.py diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index cdd723af4f..9395ab7009 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -179,6 +179,7 @@ def options(self, extra_options): "potential pair", "potential pair quite conductive", "single particle potential pair", + "set external potential", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( @@ -417,6 +418,11 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.PotentialPair2plus1D(self.param) elif self.options["current collector"] == "single particle potential pair": submodel = pybamm.current_collector.SingleParticlePotentialPair(self.param) + elif self.options["current collector"] == "set external potential": + if self.options["dimensionality"] == 1: + submodel = pybamm.current_collector.SetPotentialSingleParticle1plus1D( + self.param + ) self.submodels["current collector"] = submodel def set_voltage_variables(self): diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index 608dd80aa8..19baa4839f 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -60,8 +60,14 @@ def set_negative_electrode_submodel(self): def set_positive_electrode_submodel(self): + if self.options["current collector"] == "set external potential": + # Potentials are set by external model + set_positive_potential = False + else: + # Potential determined by 1D model + set_positive_potential = True self.submodels["positive electrode"] = pybamm.electrode.ohm.LeadingOrder( - self.param, "Positive" + self.param, "Positive", set_positive_potential=set_positive_potential ) def set_electrolyte_submodel(self): diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index 79b0c07f77..9a404409bb 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -17,3 +17,4 @@ QuiteConductivePotentialPair1plus1D, QuiteConductivePotentialPair2plus1D, ) +from .set_potential_single_particle import SetPotentialSingleParticle1plus1D diff --git a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py b/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py deleted file mode 100644 index e584ce9dbb..0000000000 --- a/pybamm/models/submodels/current_collector/one_dimensional_current_collector.py +++ /dev/null @@ -1,134 +0,0 @@ -# -# Class for one-dimensional current collectors -# -import pybamm -from .base_current_collector import BaseModel - - -class OneDimensionalCurrentCollector(BaseModel): - """A submodel 1D current collectors. - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - - - **Extends:** :class:`pybamm.current_collector.BaseModel` - """ - - def __init__(self, param): - super().__init__(param) - - def get_fundamental_variables(self): - - phi_s_cn = pybamm.standard_variables.phi_s_cn - phi_s_cp = pybamm.standard_variables.phi_s_cp - - variables = self._get_standard_potential_variables(phi_s_cn, phi_s_cp) - - # TO DO: grad not implemented for 2D yet - i_cc = pybamm.Scalar(0) - i_boundary_cc = pybamm.standard_variables.i_boundary_cc - - variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) - - return variables - -# def set_algebraic(self, variables): -# -# phi_s_cn = variables["Negative current collector potential"] -# phi_s_cp = variables["Positive current collector potential"] -# i_boundary_cc = variables["Current collector current density"] -# -# param = self.param -# applied_current = param.current_with_time -# -# self.algebraic = { -# phi_s_cn: phi_s_cn, -# phi_s_cp: phi_s_cp -# - ( -# param.U_p(param.c_p_init, param.T_ref) -# - param.U_n(param.c_n_init, param.T_ref) -# ) - 0.1*param.l_z, -# i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z, -# } - - def set_algebraic(self, variables): - - param = self.param - - ocp_p_av = variables["Average positive electrode open circuit potential"] - ocp_n_av = variables["Average negative electrode open circuit potential"] - eta_r_n_av = variables["Average negative electrode reaction overpotential"] - eta_r_p_av = variables["Average positive electrode reaction overpotential"] - eta_e_av = variables["Average electrolyte overpotential"] - delta_phi_s_n_av = variables["Average negative electrode ohmic losses"] - delta_phi_s_p_av = variables["Average positive electrode ohmic losses"] - - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = variables["Positive current collector potential"] - i_boundary_cc = variables["Current collector current density"] - v_boundary_cc = variables["Local current collector potential difference"] - applied_current = param.current_with_time - # The voltage-current expression from the SPM(e) - local_voltage_expression = ( - ocp_p_av - - ocp_n_av - + eta_r_p_av - - eta_r_n_av - + eta_e_av - + delta_phi_s_p_av - - delta_phi_s_n_av - ) - - self.algebraic = { - phi_s_cn: pybamm.div(pybamm.grad((phi_s_cn))) - - (param.sigma_cn * param.delta ** 2 / param.l_cn) - * pybamm.source(i_boundary_cc, phi_s_cn), - phi_s_cp: pybamm.div(pybamm.grad(phi_s_cp)) - + (param.sigma_cp * param.delta ** 2 / param.l_cp) - * pybamm.source(i_boundary_cc, phi_s_cp), -# i_boundary_cc: v_boundary_cc - local_voltage_expression, - i_boundary_cc: i_boundary_cc - applied_current / param.l_y / param.l_z, - } - - def set_boundary_conditions(self, variables): - - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = variables["Positive current collector potential"] - - param = self.param - applied_current = param.current_with_time - - pos_tab_bc = -applied_current / ( - param.sigma_cp * param.delta ** 2 * param.l_tab_p * param.l_cp - ) - - self.boundary_conditions = { - phi_s_cn: { - "left": (pybamm.Scalar(0), "Dirichlet"), - "right": (pybamm.Scalar(0), "Neumann"), - }, - phi_s_cp: { - "left": (pybamm.Scalar(0), "Neumann"), - "right": (pybamm.Scalar(3.0), "Dirichlet"), - }, - } - - return variables - - def set_initial_conditions(self, variables): - - param = self.param - applied_current = param.current_with_time - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = variables["Positive current collector potential"] - i_boundary_cc = variables["Current collector current density"] - - self.initial_conditions = { - phi_s_cn: pybamm.Scalar(0), - phi_s_cp: param.U_p(param.c_p_init, param.T_ref) - - param.U_n(param.c_n_init, param.T_ref), - i_boundary_cc: applied_current / param.l_y / param.l_z, - } diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py new file mode 100644 index 0000000000..0e84905a02 --- /dev/null +++ b/pybamm/models/submodels/current_collector/set_potential_single_particle.py @@ -0,0 +1,152 @@ +# +# Class for one-dimensional current collectors +# +import pybamm + + +class SetPotentialSingleParticle1plus1D(pybamm.BaseSubModel): + """A submodel 1D current collectors which *doesn't* update the potentials + during solve. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + + + **Extends:** :class:`pybamm.current_collector.BaseModel` + """ + + def __init__(self, param): + super().__init__(param) + + def _get_standard_potential_variables(self, phi_s_cn, phi_s_cp): + """ + A private function to obtain the standard variables which + can be derived from the potentials in the current collector. + + Parameters + ---------- + phi_cc : :class:`pybamm.Symbol` + The potential in the current collector. + + Returns + ------- + variables : dict + The variables which can be derived from the potential in the + current collector. + """ + param = self.param + + # Local potential difference + V_cc = phi_s_cp - phi_s_cn + + # In 2D left corresponds to the negative tab and right the positive tab + phi_neg_tab = pybamm.BoundaryValue(phi_s_cn, "left") + phi_pos_tab = pybamm.BoundaryValue(phi_s_cp, "right") + + variables = { + "Negative current collector potential": phi_s_cn, + "Negative current collector potential [V]": phi_s_cn + * param.potential_scale, + "Negative tab potential": phi_neg_tab, + "Negative tab potential [V]": phi_neg_tab * param.potential_scale, + "Positive tab potential": phi_pos_tab, + "Positive tab potential [V]": param.U_p_ref + - param.U_n_ref + + phi_pos_tab * param.potential_scale, + "Positive current collector potential": phi_s_cp, + "Positive current collector potential [V]": param.U_p_ref + - param.U_n_ref + + phi_s_cp * param.potential_scale, + "Local current collector potential difference": V_cc, + "Local current collector potential difference [V]": param.U_p_ref + - param.U_n_ref + + V_cc * param.potential_scale, + } + + return variables + + def _get_standard_current_variables(self, i_cc, i_boundary_cc): + """ + A private function to obtain the standard variables which + can be derived from the current in the current collector. + Parameters + ---------- + i_cc : :class:`pybamm.Symbol` + The current in the current collector. + i_boundary_cc : :class:`pybamm.Symbol` + The current leaving the current collector and going into the cell + Returns + ------- + variables : dict + The variables which can be derived from the current in the current + collector. + """ + + # TO DO: implement grad in 2D to get i_cc + # just need this to get 1D models working for now + variables = {"Current collector current density": i_boundary_cc} + + return variables + + def get_fundamental_variables(self): + + phi_s_cn = pybamm.standard_variables.phi_s_cn + phi_s_cp = pybamm.standard_variables.phi_s_cp + + variables = self._get_standard_potential_variables(phi_s_cn, phi_s_cp) + + # TO DO: grad not implemented for 2D yet + i_cc = pybamm.Scalar(0) + i_boundary_cc = pybamm.standard_variables.i_boundary_cc + + variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) + + return variables + + def set_rhs(self, variables): + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] + + # Dummy equations so that PyBaMM doesn't change the potentials during solve + # i.e. d_phi/d_t = 0. Potentials are set externally between steps. + self.rhs = {phi_s_cn: pybamm.Scalar(0), phi_s_cp: pybamm.Scalar(0)} + + def set_algebraic(self, variables): + ocp_p_av = variables["X-averaged positive electrode open circuit potential"] + ocp_n_av = variables["X-averaged negative electrode open circuit potential"] + eta_r_n_av = variables["X-averaged negative electrode reaction overpotential"] + eta_r_p_av = variables["X-averaged positive electrode reaction overpotential"] + eta_e_av = variables["X-averaged electrolyte overpotential"] + delta_phi_s_n_av = variables["X-averaged negative electrode ohmic losses"] + delta_phi_s_p_av = variables["X-averaged positive electrode ohmic losses"] + + i_boundary_cc = variables["Current collector current density"] + v_boundary_cc = variables["Local current collector potential difference"] + # The voltage-current expression from the SPM(e) + local_voltage_expression = ( + ocp_p_av + - ocp_n_av + + eta_r_p_av + - eta_r_n_av + + eta_e_av + + delta_phi_s_p_av + - delta_phi_s_n_av + ) + self.algebraic = {i_boundary_cc: v_boundary_cc - local_voltage_expression} + + def set_initial_conditions(self, variables): + + param = self.param + applied_current = param.current_with_time + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] + i_boundary_cc = variables["Current collector current density"] + + self.initial_conditions = { + phi_s_cn: pybamm.Scalar(0), + phi_s_cp: param.U_p(param.c_p_init, param.T_ref) + - param.U_n(param.c_n_init, param.T_ref), + i_boundary_cc: applied_current / param.l_y / param.l_z, + } diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/pybamm/models/submodels/electrode/base_electrode.py index 7047ed0239..28a9ce2446 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -17,8 +17,9 @@ class BaseElectrode(pybamm.BaseSubModel): **Extends:** :class:`pybamm.BaseSubModel` """ - def __init__(self, param, domain, reactions=None): + def __init__(self, param, domain, reactions=None, set_positive_potential=True): super().__init__(param, domain, reactions) + self.set_positive_potential = set_positive_potential def _get_standard_potential_variables(self, phi_s): """ @@ -127,21 +128,26 @@ def _get_standard_whole_cell_variables(self, variables): i_s_n = variables["Negative electrode current density"] i_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") i_s_p = variables["Positive electrode current density"] - phi_s_p = variables["Positive electrode potential"] - - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = pybamm.boundary_value(phi_s_p, "right") - v_boundary_cc = phi_s_cp - phi_s_cn i_s = pybamm.Concatenation(i_s_n, i_s_s, i_s_p) - variables = { - "Electrode current density": i_s, - "Positive current collector potential": phi_s_cp, - "Local current collector potential difference": v_boundary_cc, - "Local current collector potential difference [V]": U_ref - + v_boundary_cc * pot_scale, - } + if self.set_positive_potential: + phi_s_p = variables["Positive electrode potential"] + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = pybamm.boundary_value(phi_s_p, "right") + v_boundary_cc = phi_s_cp - phi_s_cn + + variables = { + "Electrode current density": i_s, + "Positive current collector potential": phi_s_cp, + "Local current collector potential difference": v_boundary_cc, + "Local current collector potential difference [V]": U_ref + + v_boundary_cc * pot_scale, + } + else: + variables = { + "Electrode current density": i_s, + } return variables diff --git a/pybamm/models/submodels/electrode/ohm/base_ohm.py b/pybamm/models/submodels/electrode/ohm/base_ohm.py index 461ab0be7f..758feda045 100644 --- a/pybamm/models/submodels/electrode/ohm/base_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/base_ohm.py @@ -20,8 +20,8 @@ class BaseModel(BaseElectrode): **Extends:** :class:`pybamm.electrode.BaseElectrode` """ - def __init__(self, param, domain, reactions=None): - super().__init__(param, domain, reactions) + def __init__(self, param, domain, reactions=None, set_positive_potential=True): + super().__init__(param, domain, reactions, set_positive_potential) def set_boundary_conditions(self, variables): diff --git a/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 269df31371..8a15e3aa77 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -21,8 +21,8 @@ class LeadingOrder(BaseModel): **Extends:** :class:`pybamm.electrode.ohm.BaseModel` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, set_positive_potential=True): + super().__init__(param, domain, set_positive_potential=set_positive_potential) def get_coupled_variables(self, variables): """ diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py new file mode 100644 index 0000000000..a80329ba22 --- /dev/null +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -0,0 +1,97 @@ +import pybamm +import numpy as np +import matplotlib.pyplot as plt +import sys + +# set logging level +pybamm.set_logging_level("INFO") + +# load (1+1D) SPM model +options = {"current collector": "set external potential", "dimensionality": 1} +model = pybamm.lithium_ion.SPM(options) + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry +param = model.default_parameter_values +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: 10} +# depnding on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, model.default_spatial_methods) +disc.process_model(model) + + +# define a method which updates statevector +def update_statevector(variables, statevector): + "takes in a dict of variable name and vector of updated state" + for name, new_vector in variables.items(): + var_slice = model.variables[name].y_slices + statevector[var_slice] = new_vector + return statevector[:, np.newaxis] # should be column vector + + +# define a method which takes a dimensional potential [V] and converts to the +# dimensionless potential used in pybamm +pot_scale = param.process_symbol( + pybamm.standard_parameters_lithium_ion.potential_scale +).evaluate() # potential scaled on thermal voltage +pot_ref = param.process_symbol( + pybamm.standard_parameters_lithium_ion.U_p_ref + - pybamm.standard_parameters_lithium_ion.U_n_ref +).evaluate() # positive potential measured with respect to reference OCV + + +def non_dim_potential(phi_dim, domain): + if domain == "negative": + phi = phi_dim / pot_scale + elif domain == "positive": + phi = (phi_dim - pot_ref) / pot_scale + return phi + + +# solve model -- replace with step +t_eval1 = np.linspace(0, 0.1, 10) +solution1 = model.default_solver.solve(model, t_eval1) +voltage_step1 = pybamm.ProcessedVariable( + model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh +) +current_step1 = pybamm.ProcessedVariable( + model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh +) +# update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) +phi_s_cn_dim_new = np.zeros(var_pts[var.z]) +phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) +variables = { + "Negative current collector potential": non_dim_potential( + phi_s_cn_dim_new, "negative" + ), + "Positive current collector potential": non_dim_potential( + phi_s_cp_dim_new, "positive" + ), +} +current_state = solution1.y[:, -1] +new_state = update_statevector(variables, current_state) + +# solve again -- replace with step +# use new state as initial condition +model.concatenated_initial_conditions = new_state +t_eval2 = np.linspace(0.1, 0.2, 10) +solution2 = model.default_solver.solve(model, t_eval2) +voltage_step2 = pybamm.ProcessedVariable( + model.variables["Terminal voltage [V]"], solution2.t, solution2.y, mesh=mesh +) + +# plot +plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) +plt.xlabel('t') +plt.ylabel('Voltage [V]') +plt.show() diff --git a/results/2plus1D/spm-1plus1D.py b/results/2plus1D/spm-1plus1D.py index 1837f7d241..5a833fd2d9 100644 --- a/results/2plus1D/spm-1plus1D.py +++ b/results/2plus1D/spm-1plus1D.py @@ -2,12 +2,13 @@ import numpy as np import sys import matplotlib.pyplot as plt -plt.close('all') + +plt.close("all") # set logging level pybamm.set_logging_level("INFO") # load (2+1D) SPM model -options = {"bc_options": {"dimensionality": 1}} +options = {"current collector": "set external potential", "dimensionality": 1} model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() @@ -44,7 +45,7 @@ t_eval = np.linspace(0, t_end, 10) solution = model.default_solver.solve(model, t_eval) -#e_conc = pybamm.ProcessedVariable( +# e_conc = pybamm.ProcessedVariable( # model.variables['Electrolyte concentration [mol.m-3]'], # solution.t, # solution.y, @@ -52,28 +53,28 @@ # ) # plot -#plot = pybamm.QuickPlot(model, mesh, solution) -#plot.dynamic_plot() +# plot = pybamm.QuickPlot(model, mesh, solution) +# plot.dynamic_plot() + def plot_var(var, time=-1): variable = model.variables[var] len_x = len(mesh.combine_submeshes(*variable.domain)) len_z = variable.shape[0] // len_x entries = np.empty((len_x, len_z, len(solution.t))) - + for idx in range(len(solution.t)): t = solution.t[idx] y = solution.y[:, idx] - entries[:, :, idx] = np.reshape( - variable.evaluate(t, y), [len_x, len_z] - ) + entries[:, :, idx] = np.reshape(variable.evaluate(t, y), [len_x, len_z]) plt.figure() for bat_id in range(len_x): plt.plot(range(len_z), entries[bat_id, :, time].flatten()) plt.figure() plt.imshow(entries[:, :, time]) -#plot_var(var="Electrolyte concentration") + +# plot_var(var="Electrolyte concentration") plot_var(var="Interfacial current density", time=-1) -#plot_var(var="Current collector current density", time=[0]) -#plot_var(var="Local current collector potential difference", time=[0]) +# plot_var(var="Current collector current density", time=[0]) +# plot_var(var="Local current collector potential difference", time=[0]) From 79dfecadf7a14a94d1b71d831f114532eeff7322 Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 6 Aug 2019 11:51:15 +0100 Subject: [PATCH 006/122] update set-potential --- results/2plus1D/set-potential-spm-1plus1D.py | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index a80329ba22..740008e152 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -69,7 +69,7 @@ def non_dim_potential(phi_dim, domain): ) # update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) phi_s_cn_dim_new = np.zeros(var_pts[var.z]) -phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) +phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) variables = { "Negative current collector potential": non_dim_potential( phi_s_cn_dim_new, "negative" @@ -95,3 +95,23 @@ def non_dim_potential(phi_dim, domain): plt.xlabel('t') plt.ylabel('Voltage [V]') plt.show() + +def plot_var(var, solution, time=-1): + variable = model.variables[var] + len_x = len(mesh.combine_submeshes(*variable.domain)) + len_z = variable.shape[0] // len_x + entries = np.empty((len_x, len_z, len(solution.t))) + + for idx in range(len(solution.t)): + t = solution.t[idx] + y = solution.y[:, idx] + entries[:, :, idx] = np.reshape(variable.evaluate(t, y), [len_x, len_z]) + plt.figure() + for bat_id in range(len_x): + plt.plot(range(len_z), entries[bat_id, :, time].flatten()) + plt.figure() + plt.imshow(entries[:, :, time]) + +plot_var(var="Interfacial current density", solution=solution2, time=-1) +plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) +plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) From 5659b4463296993c4829eed2fd2434ddfd13f0f6 Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 8 Aug 2019 14:27:25 +0100 Subject: [PATCH 007/122] turn on thermal --- results/2plus1D/set-potential-spm-1plus1D.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index 740008e152..5b4e012602 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -2,12 +2,14 @@ import numpy as np import matplotlib.pyplot as plt import sys - +plt.close() # set logging level pybamm.set_logging_level("INFO") # load (1+1D) SPM model -options = {"current collector": "set external potential", "dimensionality": 1} +options = {"current collector": "set external potential", + "dimensionality": 1, + "thermal": "lumped"} model = pybamm.lithium_ion.SPM(options) # create geometry @@ -69,7 +71,8 @@ def non_dim_potential(phi_dim, domain): ) # update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) phi_s_cn_dim_new = np.zeros(var_pts[var.z]) -phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) +#phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) +phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) variables = { "Negative current collector potential": non_dim_potential( phi_s_cn_dim_new, "negative" @@ -112,6 +115,8 @@ def plot_var(var, solution, time=-1): plt.figure() plt.imshow(entries[:, :, time]) -plot_var(var="Interfacial current density", solution=solution2, time=-1) -plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) +#plot_var(var="Positive current collector potential", solution=solution1, time=-1) +#plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) +#plot_var(var="Interfacial current density", solution=solution2, time=-1) +#plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) From 902ac4f50cdf183bb193229ec3ea4275bff1d5e3 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 12 Aug 2019 09:54:39 +0100 Subject: [PATCH 008/122] #548 add set external temp submodel --- .../full_battery_models/base_battery_model.py | 5 +- pybamm/models/submodels/thermal/__init__.py | 1 + .../submodels/thermal/set_temperature.py | 59 +++++++++++++++++++ results/2plus1D/set-potential-spm-1plus1D.py | 6 +- 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 pybamm/models/submodels/thermal/set_temperature.py diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 9395ab7009..8499385de9 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -192,7 +192,7 @@ def options(self, extra_options): options["dimensionality"] ) ) - if options["thermal"] not in [None, "lumped", "full"]: + if options["thermal"] not in [None, "lumped", "full", "set external temperature"]: raise pybamm.OptionError( "Unknown thermal model '{}'".format(options["thermal"]) ) @@ -404,6 +404,8 @@ def set_thermal_submodel(self): thermal_submodel = pybamm.thermal.Full(self.param) elif self.options["thermal"] == "lumped": thermal_submodel = pybamm.thermal.Lumped(self.param) + elif self.options["thermal"] == "set external temperature": + thermal_submodel = pybamm.thermal.SetTemperature(self.param) self.submodels["thermal"] = thermal_submodel @@ -423,6 +425,7 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.SetPotentialSingleParticle1plus1D( self.param ) + self.submodels["current collector"] = submodel def set_voltage_variables(self): diff --git a/pybamm/models/submodels/thermal/__init__.py b/pybamm/models/submodels/thermal/__init__.py index 372417b3a9..dd5e0b613d 100644 --- a/pybamm/models/submodels/thermal/__init__.py +++ b/pybamm/models/submodels/thermal/__init__.py @@ -2,3 +2,4 @@ from .isothermal import Isothermal from .lumped_thermal import Lumped from .full_thermal import Full +from .set_temperature import SetTemperature diff --git a/pybamm/models/submodels/thermal/set_temperature.py b/pybamm/models/submodels/thermal/set_temperature.py new file mode 100644 index 0000000000..5603462543 --- /dev/null +++ b/pybamm/models/submodels/thermal/set_temperature.py @@ -0,0 +1,59 @@ +# +# Class for thermal submodel in which the temperature is set externally +# +import pybamm + +from .base_thermal import BaseModel + + +class SetTemperature(BaseModel): + """Class for lumped thermal submodel which *doesn't update the temperature + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + + + **Extends:** :class:`pybamm.thermal.BaseModel` + """ + + def __init__(self, param): + super().__init__(param) + + def get_fundamental_variables(self): + + T_av = pybamm.standard_variables.T_av + T_n = pybamm.PrimaryBroadcast(T_av, ["negative electrode"]) + T_s = pybamm.PrimaryBroadcast(T_av, ["separator"]) + T_p = pybamm.PrimaryBroadcast(T_av, ["positive electrode"]) + T = pybamm.Concatenation(T_n, T_s, T_p) + + variables = self._get_standard_fundamental_variables(T) + return variables + + def get_coupled_variables(self, variables): + variables.update(self._get_standard_coupled_variables(variables)) + return variables + + def _flux_law(self, T): + """Fast x-direction heat diffusion (i.e. reached steady state)""" + q = pybamm.FullBroadcast( + pybamm.Scalar(0), + ["negative electrode", "separator", "positive electrode"], + "current collector", + ) + return q + + def _unpack(self, variables): + T_av = variables["X-averaged cell temperature"] + q = variables["Heat flux"] + Q_av = variables["X-averaged total heating"] + return T_av, q, Q_av + + def set_rhs(self, variables): + T_av, _, _ = self._unpack(variables) + + # Dummy equation so that PyBaMM doesn't change the temperature during solve + # i.e. d_T/d_t = 0. The (local) temperature is set externally between steps. + self.rhs = {T_av: pybamm.Scalar(0)} diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index 5b4e012602..178c643820 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -3,13 +3,14 @@ import matplotlib.pyplot as plt import sys plt.close() + # set logging level pybamm.set_logging_level("INFO") # load (1+1D) SPM model options = {"current collector": "set external potential", "dimensionality": 1, - "thermal": "lumped"} + "thermal": "set external temperature"} model = pybamm.lithium_ion.SPM(options) # create geometry @@ -69,7 +70,8 @@ def non_dim_potential(phi_dim, domain): current_step1 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh ) -# update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) + +# update potentials phi_s_cn_dim_new = np.zeros(var_pts[var.z]) #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) From 0d755158910f36a1a27d48b54bbcf5f491cdb7ac Mon Sep 17 00:00:00 2001 From: tom Date: Mon, 12 Aug 2019 10:15:18 +0100 Subject: [PATCH 009/122] update voltage BCs --- results/2plus1D/set-potential-spm-1plus1D.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index 5b4e012602..67eb3eaa64 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -2,7 +2,7 @@ import numpy as np import matplotlib.pyplot as plt import sys -plt.close() +plt.close('all') # set logging level pybamm.set_logging_level("INFO") @@ -69,10 +69,15 @@ def non_dim_potential(phi_dim, domain): current_step1 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh ) + +current_state = solution1.y[:, -1] + # update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) -phi_s_cn_dim_new = np.zeros(var_pts[var.z]) +#phi_s_cn_dim_new = np.zeros(var_pts[var.z]) +phi_s_cn_dim_new = current_state[model.variables["Negative current collector potential"].y_slices] * 0 #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) -phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) +#phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) +phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - 0.05 * np.linspace(0, 1, var_pts[var.z]) variables = { "Negative current collector potential": non_dim_potential( phi_s_cn_dim_new, "negative" @@ -81,7 +86,7 @@ def non_dim_potential(phi_dim, domain): phi_s_cp_dim_new, "positive" ), } -current_state = solution1.y[:, -1] + new_state = update_statevector(variables, current_state) # solve again -- replace with step @@ -112,11 +117,16 @@ def plot_var(var, solution, time=-1): plt.figure() for bat_id in range(len_x): plt.plot(range(len_z), entries[bat_id, :, time].flatten()) + plt.title(var) plt.figure() plt.imshow(entries[:, :, time]) + plt.title(var) #plot_var(var="Positive current collector potential", solution=solution1, time=-1) -#plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) +plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) #plot_var(var="Interfacial current density", solution=solution2, time=-1) #plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) + +var_names = list(model.variables.keys()) +var_names.sort() \ No newline at end of file From 2a2635f574e0281c1bced5b8f54584b0453f390f Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 12 Aug 2019 10:48:28 +0100 Subject: [PATCH 010/122] #548 add heating plot --- results/2plus1D/set-potential-spm-1plus1D.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index e180af1bfe..22d8c6fa5f 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -70,6 +70,10 @@ def non_dim_potential(phi_dim, domain): current_step1 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh ) +heating_step1 = pybamm.ProcessedVariable( + model.variables["X-averaged total heating [A.V.m-3]"], solution1.t, solution1.y, mesh=mesh +) + current_state = solution1.y[:, -1] @@ -99,12 +103,21 @@ def non_dim_potential(phi_dim, domain): voltage_step2 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution2.t, solution2.y, mesh=mesh ) +heating_step2 = pybamm.ProcessedVariable( + model.variables["X-averaged total heating [A.V.m-3]"], solution2.t, solution2.y, mesh=mesh +) # plot plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) plt.xlabel('t') plt.ylabel('Voltage [V]') plt.show() +z = np.linspace(0, 1, 10) +plt.plot(t_eval1, heating_step1(t_eval1, z=z), t_eval2, heating_step2(t_eval2, z=z)) +plt.xlabel('t') +plt.ylabel('X-averaged total heating [A.V.m-3]') +plt.show() + def plot_var(var, solution, time=-1): variable = model.variables[var] @@ -128,7 +141,7 @@ def plot_var(var, solution, time=-1): plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) #plot_var(var="Interfacial current density", solution=solution2, time=-1) #plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) -plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) +#plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) var_names = list(model.variables.keys()) var_names.sort() From ae9668cf1e40382baf57d0313a17f8544eb4a888 Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 20 Aug 2019 12:30:48 +0100 Subject: [PATCH 011/122] test no voltage update --- results/2plus1D/set-potential-spm-1plus1D.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index 22d8c6fa5f..646170043d 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -79,11 +79,13 @@ def non_dim_potential(phi_dim, domain): # update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) #phi_s_cn_dim_new = np.zeros(var_pts[var.z]) -phi_s_cn_dim_new = current_state[model.variables["Negative current collector potential"].y_slices] * 0 +sf_cn = 1.0 +phi_s_cn_dim_new = current_state[model.variables["Negative current collector potential"].y_slices] * sf_cn #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) -phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - 0.05 * np.linspace(0, 1, var_pts[var.z]) +sf_cp = 0.0 +phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf * np.linspace(0, 1, var_pts[var.z]) variables = { "Negative current collector potential": non_dim_potential( phi_s_cn_dim_new, "negative" @@ -93,11 +95,12 @@ def non_dim_potential(phi_dim, domain): ), } -new_state = update_statevector(variables, current_state) +#new_state = update_statevector(variables, current_state) # solve again -- replace with step # use new state as initial condition -model.concatenated_initial_conditions = new_state +#model.concatenated_initial_conditions = new_state +model.concatenated_initial_conditions = current_state[np.newaxis, :] t_eval2 = np.linspace(0.1, 0.2, 10) solution2 = model.default_solver.solve(model, t_eval2) voltage_step2 = pybamm.ProcessedVariable( @@ -108,14 +111,17 @@ def non_dim_potential(phi_dim, domain): ) # plot +plt.figure() plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) plt.xlabel('t') plt.ylabel('Voltage [V]') plt.show() +plt.figure() z = np.linspace(0, 1, 10) plt.plot(t_eval1, heating_step1(t_eval1, z=z), t_eval2, heating_step2(t_eval2, z=z)) plt.xlabel('t') plt.ylabel('X-averaged total heating [A.V.m-3]') +plt.yscale('log') plt.show() @@ -138,7 +144,7 @@ def plot_var(var, solution, time=-1): plt.title(var) #plot_var(var="Positive current collector potential", solution=solution1, time=-1) -plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) +#plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) #plot_var(var="Interfacial current density", solution=solution2, time=-1) #plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) #plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) From 45ec923f1cbe886b7363fc7ea33be678550703cb Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 20 Aug 2019 13:41:59 +0100 Subject: [PATCH 012/122] take out non-dimensionalizing --- results/2plus1D/set-potential-spm-1plus1D.py | 72 +++++++++++++------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index 646170043d..c636010091 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -22,8 +22,9 @@ param.process_geometry(geometry) # set mesh +nbat = 10 var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: 10} +var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: nbat} # depnding on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -62,17 +63,20 @@ def non_dim_potential(phi_dim, domain): # solve model -- replace with step -t_eval1 = np.linspace(0, 0.1, 10) +t_eval1 = np.linspace(0, 0.1, 20) solution1 = model.default_solver.solve(model, t_eval1) voltage_step1 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh ) current_step1 = pybamm.ProcessedVariable( - model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh + model.variables["Current [A]"], solution1.t, solution1.y, mesh=mesh ) heating_step1 = pybamm.ProcessedVariable( model.variables["X-averaged total heating [A.V.m-3]"], solution1.t, solution1.y, mesh=mesh ) +particle_step1 = pybamm.ProcessedVariable( + model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution1.t, solution1.y, mesh=mesh +) current_state = solution1.y[:, -1] @@ -84,45 +88,67 @@ def non_dim_potential(phi_dim, domain): #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) -sf_cp = 0.0 -phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf * np.linspace(0, 1, var_pts[var.z]) +sf_cp = 1e-2 +phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf_cp * np.linspace(0, 1, var_pts[var.z]) +#variables = { +# "Negative current collector potential": non_dim_potential( +# phi_s_cn_dim_new, "negative" +# ), +# "Positive current collector potential": non_dim_potential( +# phi_s_cp_dim_new, "positive" +# ), +#} + variables = { - "Negative current collector potential": non_dim_potential( - phi_s_cn_dim_new, "negative" - ), - "Positive current collector potential": non_dim_potential( - phi_s_cp_dim_new, "positive" - ), + "Negative current collector potential": phi_s_cn_dim_new, + "Positive current collector potential": phi_s_cp_dim_new, } -#new_state = update_statevector(variables, current_state) +new_state = update_statevector(variables, current_state) # solve again -- replace with step # use new state as initial condition -#model.concatenated_initial_conditions = new_state -model.concatenated_initial_conditions = current_state[np.newaxis, :] -t_eval2 = np.linspace(0.1, 0.2, 10) +model.concatenated_initial_conditions = new_state +#model.concatenated_initial_conditions = current_state[:, np.newaxis] +t_eval2 = np.linspace(0.1, 0.2, 20) solution2 = model.default_solver.solve(model, t_eval2) voltage_step2 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution2.t, solution2.y, mesh=mesh ) +current_step2 = pybamm.ProcessedVariable( + model.variables["Current [A]"], solution2.t, solution2.y, mesh=mesh +) heating_step2 = pybamm.ProcessedVariable( model.variables["X-averaged total heating [A.V.m-3]"], solution2.t, solution2.y, mesh=mesh ) - +particle_step2 = pybamm.ProcessedVariable( + model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution2.t, solution2.y, mesh=mesh +) # plot -plt.figure() -plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) -plt.xlabel('t') -plt.ylabel('Voltage [V]') -plt.show() -plt.figure() +#plt.figure() +#plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) +#plt.xlabel('t') +#plt.ylabel('Voltage [V]') +#plt.show() +#plt.figure() +#plt.plot(t_eval1, current_step1(t_eval1), t_eval2, current_step2(t_eval2)) +#plt.xlabel('t') +#plt.ylabel('Current [A]') +#plt.show() +#plt.figure() z = np.linspace(0, 1, 10) -plt.plot(t_eval1, heating_step1(t_eval1, z=z), t_eval2, heating_step2(t_eval2, z=z)) +for bat_id in range(nbat): + plt.plot(t_eval1, heating_step1(t_eval1, z=z)[bat_id, :], t_eval2, heating_step2(t_eval2, z=z)[bat_id, :]) plt.xlabel('t') plt.ylabel('X-averaged total heating [A.V.m-3]') plt.yscale('log') plt.show() +plt.figure() +for bat_id in range(nbat): + plt.plot(t_eval1, particle_step1(t_eval1,z=z)[bat_id, :], t_eval2, particle_step2(t_eval2,z=z)[bat_id, :]) +plt.xlabel('t') +plt.ylabel('X-averaged positive particle surface concentration [mol.m-3]') +plt.show() def plot_var(var, solution, time=-1): From 411838122903ae63e44a2eee260bb87ad23a3419 Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 22 Aug 2019 11:32:44 +0100 Subject: [PATCH 013/122] add temperature update wip --- results/2plus1D/set-potential-spm-1plus1D.py | 46 ++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index c636010091..1252798e51 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -33,6 +33,8 @@ disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) +t_sec = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge).evaluate() +t_hour = t_sec/(3600) # define a method which updates statevector def update_statevector(variables, statevector): @@ -88,8 +90,14 @@ def non_dim_potential(phi_dim, domain): #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) -sf_cp = 1e-2 +sf_cp = 0.0 # 5e-2 phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf_cp * np.linspace(0, 1, var_pts[var.z]) + +temp_ave = current_state[model.variables["X-averaged cell temperature"].y_slices] +temp_neg = current_state[model.variables["X-averaged negative electrode temperature"].y_slices] +temp_pos = current_state[model.variables["X-averaged positive electrode temperature"].y_slices] +temp_sep = current_state[model.variables["X-averaged separator temperature"].y_slices] + #variables = { # "Negative current collector potential": non_dim_potential( # phi_s_cn_dim_new, "negative" @@ -98,10 +106,14 @@ def non_dim_potential(phi_dim, domain): # phi_s_cp_dim_new, "positive" # ), #} - +dt = np.linspace(0, 1, len(temp_ave))*1.0 variables = { "Negative current collector potential": phi_s_cn_dim_new, "Positive current collector potential": phi_s_cp_dim_new, + "X-averaged cell temperature": temp_ave + dt, + "X-averaged negative electrode temperature": temp_neg + dt, + "X-averaged positive electrode temperature": temp_pos + dt, + "X-averaged separator temperature": temp_sep + dt, } new_state = update_statevector(variables, current_state) @@ -125,28 +137,28 @@ def non_dim_potential(phi_dim, domain): model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution2.t, solution2.y, mesh=mesh ) # plot -#plt.figure() -#plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) -#plt.xlabel('t') -#plt.ylabel('Voltage [V]') -#plt.show() -#plt.figure() -#plt.plot(t_eval1, current_step1(t_eval1), t_eval2, current_step2(t_eval2)) -#plt.xlabel('t') -#plt.ylabel('Current [A]') -#plt.show() -#plt.figure() +plt.figure() +plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) +plt.xlabel('t') +plt.ylabel('Voltage [V]') +plt.show() +plt.figure() +plt.plot(t_eval1, current_step1(t_eval1), t_eval2, current_step2(t_eval2)) +plt.xlabel('t') +plt.ylabel('Current [A]') +plt.show() +plt.figure() z = np.linspace(0, 1, 10) for bat_id in range(nbat): - plt.plot(t_eval1, heating_step1(t_eval1, z=z)[bat_id, :], t_eval2, heating_step2(t_eval2, z=z)[bat_id, :]) -plt.xlabel('t') + plt.plot(t_eval1*t_hour, heating_step1(t_eval1, z=z)[bat_id, :], t_eval2*t_hour, heating_step2(t_eval2, z=z)[bat_id, :]) +plt.xlabel('t [hrs]') plt.ylabel('X-averaged total heating [A.V.m-3]') plt.yscale('log') plt.show() plt.figure() for bat_id in range(nbat): - plt.plot(t_eval1, particle_step1(t_eval1,z=z)[bat_id, :], t_eval2, particle_step2(t_eval2,z=z)[bat_id, :]) -plt.xlabel('t') + plt.plot(t_eval1*t_hour, particle_step1(t_eval1,z=z)[bat_id, :], t_eval2*t_hour, particle_step2(t_eval2,z=z)[bat_id, :]) +plt.xlabel('t [hrs]') plt.ylabel('X-averaged positive particle surface concentration [mol.m-3]') plt.show() From 44e25a3995e7b507f7312fea95639927b71d7a41 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 4 Sep 2019 10:54:03 +0100 Subject: [PATCH 014/122] updates --- results/2plus1D/set-potential-spm-1plus1D.py | 49 ++++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-potential-spm-1plus1D.py index 1252798e51..1bf10cb698 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-potential-spm-1plus1D.py @@ -8,8 +8,10 @@ pybamm.set_logging_level("INFO") # load (1+1D) SPM model -options = {"current collector": "set external potential", - "dimensionality": 1, +#options = {"current collector": "set external potential", +# "dimensionality": 1, +# "thermal": "set external temperature"} +options = {"dimensionality": 1, "thermal": "set external temperature"} model = pybamm.lithium_ion.SPM(options) @@ -22,7 +24,7 @@ param.process_geometry(geometry) # set mesh -nbat = 10 +nbat = 108 var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: nbat} # depnding on number of points in y-z plane may need to increase recursion depth... @@ -63,6 +65,10 @@ def non_dim_potential(phi_dim, domain): phi = (phi_dim - pot_ref) / pot_scale return phi +def non_dim_temperature(temperature): + Delta_T = param.process_symbol(model.submodels['thermal'].param.Delta_T).evaluate() + T_ref = param.process_symbol(model.submodels['thermal'].param.T_ref).evaluate() + return (temperature - T_ref)/Delta_T # solve model -- replace with step t_eval1 = np.linspace(0, 0.1, 20) @@ -79,19 +85,21 @@ def non_dim_potential(phi_dim, domain): particle_step1 = pybamm.ProcessedVariable( model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution1.t, solution1.y, mesh=mesh ) - +temperature_step1 = pybamm.ProcessedVariable( + model.variables["X-averaged cell temperature [K]"], solution1.t, solution1.y, mesh=mesh +) current_state = solution1.y[:, -1] # update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) #phi_s_cn_dim_new = np.zeros(var_pts[var.z]) sf_cn = 1.0 -phi_s_cn_dim_new = current_state[model.variables["Negative current collector potential"].y_slices] * sf_cn +#phi_s_cn_dim_new = current_state[model.variables["Negative current collector potential"].y_slices] * sf_cn #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) #phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) sf_cp = 0.0 # 5e-2 -phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf_cp * np.linspace(0, 1, var_pts[var.z]) +#phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf_cp * np.linspace(0, 1, var_pts[var.z]) temp_ave = current_state[model.variables["X-averaged cell temperature"].y_slices] temp_neg = current_state[model.variables["X-averaged negative electrode temperature"].y_slices] @@ -106,14 +114,17 @@ def non_dim_potential(phi_dim, domain): # phi_s_cp_dim_new, "positive" # ), #} -dt = np.linspace(0, 1, len(temp_ave))*1.0 + +T_ref = param.process_symbol(model.submodels['thermal'].param.T_ref).evaluate() +t_external = np.linspace(T_ref, T_ref + 6.0, nbat) +non_dim_t_external = non_dim_temperature(t_external) variables = { - "Negative current collector potential": phi_s_cn_dim_new, - "Positive current collector potential": phi_s_cp_dim_new, - "X-averaged cell temperature": temp_ave + dt, - "X-averaged negative electrode temperature": temp_neg + dt, - "X-averaged positive electrode temperature": temp_pos + dt, - "X-averaged separator temperature": temp_sep + dt, +# "Negative current collector potential": phi_s_cn_dim_new, +# "Positive current collector potential": phi_s_cp_dim_new, + "X-averaged cell temperature": non_dim_t_external, + "X-averaged negative electrode temperature": non_dim_t_external, + "X-averaged positive electrode temperature": non_dim_t_external, + "X-averaged separator temperature": non_dim_t_external, } new_state = update_statevector(variables, current_state) @@ -136,6 +147,9 @@ def non_dim_potential(phi_dim, domain): particle_step2 = pybamm.ProcessedVariable( model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution2.t, solution2.y, mesh=mesh ) +temperature_step2 = pybamm.ProcessedVariable( + model.variables["X-averaged cell temperature [K]"], solution2.t, solution2.y, mesh=mesh +) # plot plt.figure() plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) @@ -148,7 +162,7 @@ def non_dim_potential(phi_dim, domain): plt.ylabel('Current [A]') plt.show() plt.figure() -z = np.linspace(0, 1, 10) +z = np.linspace(0, 1, nbat) for bat_id in range(nbat): plt.plot(t_eval1*t_hour, heating_step1(t_eval1, z=z)[bat_id, :], t_eval2*t_hour, heating_step2(t_eval2, z=z)[bat_id, :]) plt.xlabel('t [hrs]') @@ -161,7 +175,12 @@ def non_dim_potential(phi_dim, domain): plt.xlabel('t [hrs]') plt.ylabel('X-averaged positive particle surface concentration [mol.m-3]') plt.show() - +plt.figure() +for bat_id in range(nbat): + plt.plot(t_eval1*t_hour, temperature_step1(t_eval1,z=z)[bat_id, :], t_eval2*t_hour, temperature_step2(t_eval2,z=z)[bat_id, :]) +plt.xlabel('t [hrs]') +plt.ylabel('X-averaged cell temperature [K]') +plt.show() def plot_var(var, solution, time=-1): variable = model.variables[var] From 3b9d6238a9447ecdceaa19fb8135f83f40ac155d Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 4 Sep 2019 16:15:49 +0100 Subject: [PATCH 015/122] #548 jelly roll potential pair --- .../full_battery_models/base_battery_model.py | 3 ++ .../submodels/current_collector/__init__.py | 1 + .../current_collector/potential_pair.py | 37 ++++++++++++++- ...us1D.py => set-temperature-spm-1plus1D.py} | 46 +++++++++---------- 4 files changed, 61 insertions(+), 26 deletions(-) rename results/2plus1D/{set-potential-spm-1plus1D.py => set-temperature-spm-1plus1D.py} (86%) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 8499385de9..4b5e503811 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -180,6 +180,7 @@ def options(self, extra_options): "potential pair quite conductive", "single particle potential pair", "set external potential", + "jelly roll", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( @@ -425,6 +426,8 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.SetPotentialSingleParticle1plus1D( self.param ) + elif self.options["current collector"] == "jelly roll": + submodel = pybamm.current_collector.PotentialPairUnrolled(self.param) self.submodels["current collector"] = submodel diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index a71756c6d6..47b084cc65 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -7,6 +7,7 @@ BasePotentialPair, PotentialPair1plus1D, PotentialPair2plus1D, + PotentialPairUnrolled ) from .composite_potential_pair import ( BaseCompositePotentialPair, diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index efcd7b8b95..7242ca1517 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -73,7 +73,7 @@ def set_initial_conditions(self, variables): class PotentialPair1plus1D(BasePotentialPair): - "Base class for a 1+1D potential pair model" + "Base class for a 1+1D potential pair model. Note: assumes both tabs at top" def __init__(self, param): super().__init__(param) @@ -159,3 +159,38 @@ def set_boundary_conditions(self, variables): def _get_effective_current_collector_area(self): "Return the area of the current collector" return self.param.l_y * self.param.l_z + + +class PotentialPairUnrolled(PotentialPair1plus1D): + "Base class for 1+1D model used to model an jelly roll with a tab at either end" + + def set_boundary_conditions(self, variables): + + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] + + param = self.param + applied_current = param.current_with_time + cc_area = self._get_effective_current_collector_area() + + # cc_area appears here due to choice of non-dimensionalisation + pos_tab_bc = ( + -applied_current + * cc_area + / (param.sigma_cp * param.delta ** 2 * param.l_cp) + ) + + # Boundary condition needs to be on the variables that go into the Laplacian, + # even though phi_s_cp isn't a pybamm.Variable object + self.boundary_conditions = { + phi_s_cn: { + "left": (pybamm.Scalar(0), "Dirichlet"), + "right": (pybamm.Scalar(0), "Neumann"), + }, + phi_s_cp: { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (pos_tab_bc, "Neumann"), + }, + } + + diff --git a/results/2plus1D/set-potential-spm-1plus1D.py b/results/2plus1D/set-temperature-spm-1plus1D.py similarity index 86% rename from results/2plus1D/set-potential-spm-1plus1D.py rename to results/2plus1D/set-temperature-spm-1plus1D.py index 1bf10cb698..f7deea149c 100644 --- a/results/2plus1D/set-potential-spm-1plus1D.py +++ b/results/2plus1D/set-temperature-spm-1plus1D.py @@ -8,10 +8,8 @@ pybamm.set_logging_level("INFO") # load (1+1D) SPM model -#options = {"current collector": "set external potential", -# "dimensionality": 1, -# "thermal": "set external temperature"} -options = {"dimensionality": 1, +options = {"current collector": "jelly roll", + "dimensionality": 1, "thermal": "set external temperature"} model = pybamm.lithium_ion.SPM(options) @@ -38,6 +36,7 @@ t_sec = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge).evaluate() t_hour = t_sec/(3600) + # define a method which updates statevector def update_statevector(variables, statevector): "takes in a dict of variable name and vector of updated state" @@ -73,6 +72,12 @@ def non_dim_temperature(temperature): # solve model -- replace with step t_eval1 = np.linspace(0, 0.1, 20) solution1 = model.default_solver.solve(model, t_eval1) +phi_s_cn_step1 = pybamm.ProcessedVariable( + model.variables["Negative current collector potential [V]"], solution1.t, solution1.y, mesh=mesh +) +phi_s_cp_step1 = pybamm.ProcessedVariable( + model.variables["Positive current collector potential [V]"], solution1.t, solution1.y, mesh=mesh +) voltage_step1 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh ) @@ -91,36 +96,15 @@ def non_dim_temperature(temperature): current_state = solution1.y[:, -1] -# update potentials (e.g. zero volts on neg. current collector, 3.3 volts on pos.) -#phi_s_cn_dim_new = np.zeros(var_pts[var.z]) -sf_cn = 1.0 -#phi_s_cn_dim_new = current_state[model.variables["Negative current collector potential"].y_slices] * sf_cn - -#phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) - 0.05 * np.linspace(0, 1, var_pts[var.z]) -#phi_s_cp_dim_new = 3.3 * np.ones(var_pts[var.z]) -sf_cp = 0.0 # 5e-2 -#phi_s_cp_dim_new = current_state[model.variables["Positive current collector potential"].y_slices] - sf_cp * np.linspace(0, 1, var_pts[var.z]) - temp_ave = current_state[model.variables["X-averaged cell temperature"].y_slices] temp_neg = current_state[model.variables["X-averaged negative electrode temperature"].y_slices] temp_pos = current_state[model.variables["X-averaged positive electrode temperature"].y_slices] temp_sep = current_state[model.variables["X-averaged separator temperature"].y_slices] -#variables = { -# "Negative current collector potential": non_dim_potential( -# phi_s_cn_dim_new, "negative" -# ), -# "Positive current collector potential": non_dim_potential( -# phi_s_cp_dim_new, "positive" -# ), -#} - T_ref = param.process_symbol(model.submodels['thermal'].param.T_ref).evaluate() t_external = np.linspace(T_ref, T_ref + 6.0, nbat) non_dim_t_external = non_dim_temperature(t_external) variables = { -# "Negative current collector potential": phi_s_cn_dim_new, -# "Positive current collector potential": phi_s_cp_dim_new, "X-averaged cell temperature": non_dim_t_external, "X-averaged negative electrode temperature": non_dim_t_external, "X-averaged positive electrode temperature": non_dim_t_external, @@ -135,6 +119,12 @@ def non_dim_temperature(temperature): #model.concatenated_initial_conditions = current_state[:, np.newaxis] t_eval2 = np.linspace(0.1, 0.2, 20) solution2 = model.default_solver.solve(model, t_eval2) +phi_s_cn_step2 = pybamm.ProcessedVariable( + model.variables["Negative current collector potential [V]"], solution2.t, solution2.y, mesh=mesh +) +phi_s_cp_step2 = pybamm.ProcessedVariable( + model.variables["Positive current collector potential [V]"], solution2.t, solution2.y, mesh=mesh +) voltage_step2 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution2.t, solution2.y, mesh=mesh ) @@ -152,6 +142,12 @@ def non_dim_temperature(temperature): ) # plot plt.figure() +z = np.linspace(0, 1, nbat) +for bat_id in range(nbat): + plt.plot(t_eval1*t_hour, phi_s_cp_step1(t_eval1, z=z)[bat_id, :] - phi_s_cn_step1(t_eval1, z=z)[bat_id, :], t_eval2*t_hour, phi_s_cp_step2(t_eval2, z=z)[bat_id, :] - phi_s_cn_step2(t_eval2, z=z)[bat_id, :]) +plt.xlabel('t [hrs]') +plt.ylabel('Local voltage [V]') +plt.figure() plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) plt.xlabel('t') plt.ylabel('Voltage [V]') From ea26389c4a07a9ced34ae66064c32b37817599bb Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 11 Sep 2019 15:43:31 +0100 Subject: [PATCH 016/122] #548 update step script --- pybamm/solvers/base_solver.py | 117 ++++++++++ pybamm/solvers/dae_solver.py | 119 +++++----- pybamm/solvers/ode_solver.py | 112 ++++----- .../2plus1D/set-temperature-spm-1plus1D.py | 214 +++++++++++------- 4 files changed, 344 insertions(+), 218 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 48023269fb..c9fe1a8db1 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -1,6 +1,8 @@ # # Base solver class # +import pybamm +import numpy as np class BaseSolver(object): @@ -44,7 +46,122 @@ def solve(self, model, t_eval): The times at which to compute the solution """ + pybamm.logger.info("Start solving {}".format(model.name)) + # Set up + timer = pybamm.Timer() + start_time = timer.time() + self.set_up(model) + set_up_time = timer.time() - start_time + + # Solve + solution, solve_time, termination = self.compute_solution(model, t_eval) + + # Assign times + solution.solve_time = solve_time + solution.total_time = timer.time() - start_time + solution.set_up_time = set_up_time + + pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) + pybamm.logger.info( + "Set-up time: {}, Solve time: {}, Total time: {}".format( + timer.format(solution.set_up_time), + timer.format(solution.solve_time), + timer.format(solution.total_time), + ) + ) + return solution + + def step(self, model, dt, npts=2): + """Step the solution of the model forward by a given time increment. The + first time this method is called it computes the necessary setup by + calling `self.set_up(model)`. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + dt : numeric type + The timestep over which to step the solution + npts : int, optional + The number of points at which the solution will be returned during + the step dt. Defualt is 2 (returns the solution at t0 and t0 + dt). + + """ + # Set timer + timer = pybamm.Timer() + set_up_time = None + + # Run set up on first step + if not hasattr(self, 'y0'): + start_time = timer.time() + self.set_up(model) + self.t = 0.0 + set_up_time = timer.time() - start_time + + # Step + pybamm.logger.info("Start stepping {}".format(model.name)) + t_eval = np.linspace(self.t, self.t + dt, npts) + solution, solve_time, termination = self.compute_solution(model, t_eval) + + # Assign times + solution.solve_time = solve_time + if set_up_time: + solution.total_time = timer.time() - start_time + solution.set_up_time = set_up_time + + # Set self.t and self.y0 to their values at the final step + self.t = solution.t[-1] + self.y0 = solution.y[:, -1] + + pybamm.logger.info("Finish stepping {} ({})".format(model.name, termination)) + if set_up_time: + pybamm.logger.info( + "Set-up time: {}, Step time: {}, Total time: {}".format( + timer.format(solution.set_up_time), + timer.format(solution.solve_time), + timer.format(solution.total_time), + ) + ) + else: + pybamm.logger.info( + "Step time: {}".format( + timer.format(solution.solve_time), + ) + ) + return solution + + def compute_solution(self, model, t_eval): + """Calculate the solution of the model at specified times. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + t_eval : numeric type + The times at which to compute the solution + + """ + raise NotImplementedError + + def set_up(self, model): + """Unpack model, perform checks, simplify and calculate jacobian. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + + Raises + ------ + :class:`pybamm.SolverError` + If the model contains any algebraic equations (in which case a DAE solver + should be used instead) + + """ raise NotImplementedError def get_termination_reason(self, solution, events): diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index a43de1ddb0..b0f3f1db88 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -55,7 +55,7 @@ def max_steps(self): def max_steps(self, max_steps): self._max_steps = max_steps - def solve(self, model, t_eval): + def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. Parameters @@ -67,78 +67,24 @@ def solve(self, model, t_eval): The times at which to compute the solution """ - pybamm.logger.info("Start solving {}".format(model.name)) - - # Set up timer = pybamm.Timer() - start_time = timer.time() - concatenated_rhs, concatenated_algebraic, y0, model_events, jac = self.set_up( - model - ) - set_up_time = timer.time() - start_time - - # get mass matrix entries - mass_matrix = model.mass_matrix.entries - def residuals(t, y, ydot): - pybamm.logger.debug( - "Evaluating residuals for {} at t={}".format(model.name, t) - ) - y = y[:, np.newaxis] - rhs_eval, known_evals = concatenated_rhs.evaluate(t, y, known_evals={}) - # reuse known_evals - alg_eval = concatenated_algebraic.evaluate(t, y, known_evals=known_evals)[0] - # turn into 1D arrays - rhs_eval = rhs_eval[:, 0] - alg_eval = alg_eval[:, 0] - return np.concatenate((rhs_eval, alg_eval)) - mass_matrix @ ydot - - # Create event-dependent function to evaluate events - def event_fun(event): - def eval_event(t, y): - return event.evaluate(t, y) - - return eval_event - - events = [event_fun(event) for event in model_events.values()] - - # Create function to evaluate jacobian - if jac is not None: - - def jacobian(t, y): - return jac.evaluate(t, y, known_evals={})[0] - - else: - jacobian = None - - # Solve solve_start_time = timer.time() pybamm.logger.info("Calling DAE solver") solution = self.integrate( - residuals, - y0, + self.residuals, + self.y0, t_eval, - events=events, - mass_matrix=mass_matrix, - jacobian=jacobian, + events=self.event_funs, + mass_matrix=model.mass_matrix.entries, + jacobian=self.jacobian, ) - # Assign times - solution.solve_time = timer.time() - solve_start_time - solution.total_time = timer.time() - start_time - solution.set_up_time = set_up_time + solve_time = timer.time() - solve_start_time # Identify the event that caused termination - termination = self.get_termination_reason(solution, model_events) - - pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) - pybamm.logger.info( - "Set-up time: {}, Solve time: {}, Total time: {}".format( - timer.format(solution.set_up_time), - timer.format(solution.solve_time), - timer.format(solution.total_time), - ) - ) - return solution + termination = self.get_termination_reason(solution, self.events) + + return solution, solve_time, termination def set_up(self, model): """Unpack model, perform checks, simplify and calculate jacobian. @@ -168,7 +114,7 @@ def set_up(self, model): If the model contains any algebraic equations (in which case a DAE solver should be used instead) """ - # create simplified rhs algebraic and event expressions + # create simplified rhs, algebraic and event expressions concatenated_rhs = model.concatenated_rhs concatenated_algebraic = model.concatenated_algebraic events = model.events @@ -235,7 +181,48 @@ def algebraic(t, y): # can use DAE solver to solve ODE model y0 = model.concatenated_initial_conditions[:, 0] - return concatenated_rhs, concatenated_algebraic, y0, events, jac + # Create functions to evaluate residuals + def residuals(t, y, ydot): + pybamm.logger.debug( + "Evaluating residuals for {} at t={}".format(model.name, t) + ) + y = y[:, np.newaxis] + rhs_eval, known_evals = concatenated_rhs.evaluate(t, y, known_evals={}) + # reuse known_evals + alg_eval = concatenated_algebraic.evaluate(t, y, known_evals=known_evals)[0] + # turn into 1D arrays + rhs_eval = rhs_eval[:, 0] + alg_eval = alg_eval[:, 0] + return ( + np.concatenate((rhs_eval, alg_eval)) - model.mass_matrix.entries @ ydot + ) + + # Create event-dependent function to evaluate events + def event_fun(event): + def eval_event(t, y): + return event.evaluate(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in events.values()] + + # Create function to evaluate jacobian + if jac is not None: + + def jacobian(t, y): + return jac.evaluate(t, y, known_evals={})[0] + + else: + jacobian = None + + # Add the solver attributes + self.y0 = y0 + self.rhs = rhs + self.algebraic = algebraic + self.residuals = residuals + self.events = events + self.event_funs = event_funs + self.jacobian = jacobian def calculate_consistent_initial_conditions( self, rhs, algebraic, y0_guess, jac=None diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index 958cc97ab6..6d4ae5c3d8 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -17,7 +17,7 @@ class OdeSolver(pybamm.BaseSolver): def __init__(self, method=None, tol=1e-8): super().__init__(method, tol) - def solve(self, model, t_eval): + def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. Parameters @@ -29,68 +29,24 @@ def solve(self, model, t_eval): The times at which to compute the solution """ - pybamm.logger.info("Start solving {}".format(model.name)) - - # Set up timer = pybamm.Timer() - start_time = timer.time() - concatenated_rhs, y0, model_events, jac_rhs = self.set_up(model) - set_up_time = timer.time() - start_time - - # Create function to evaluate rhs - def dydt(t, y): - pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) - y = y[:, np.newaxis] - dy = concatenated_rhs.evaluate(t, y, known_evals={})[0] - return dy[:, 0] - - # Create event-dependent function to evaluate events - def event_fun(event): - def eval_event(t, y): - return event.evaluate(t, y) - - return eval_event - events = [event_fun(event) for event in model_events.values()] - - # Create function to evaluate jacobian - if jac_rhs is not None: - - def jacobian(t, y): - return jac_rhs.evaluate(t, y, known_evals={})[0] - - else: - jacobian = None - - # Solve solve_start_time = timer.time() pybamm.logger.info("Calling ODE solver") solution = self.integrate( - dydt, - y0, + self.dydt, + self.y0, t_eval, - events=events, + events=self.event_funs, mass_matrix=model.mass_matrix.entries, - jacobian=jacobian, + jacobian=self.jacobian, ) - - # Assign times - solution.solve_time = timer.time() - solve_start_time - solution.total_time = timer.time() - start_time - solution.set_up_time = set_up_time + solve_time = timer.time() - solve_start_time # Identify the event that caused termination - termination = self.get_termination_reason(solution, model_events) - - pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) - pybamm.logger.info( - "Set-up time: {}, Solve time: {}, Total time: {}".format( - timer.format(solution.set_up_time), - timer.format(solution.solve_time), - timer.format(solution.total_time), - ) - ) - return solution + termination = self.get_termination_reason(solution, self.events) + + return solution, solve_time, termination def set_up(self, model): """Unpack model, perform checks, simplify and calculate jacobian. @@ -101,17 +57,6 @@ def set_up(self, model): The model whose solution to calculate. Must have attributes rhs and initial_conditions - Returns - ------- - concatenated_rhs : :class:`pybamm.Concatenation` - Right-hand side of differential equations - y0 : :class:`numpy.array` - Vector of initial conditions - events : dict - Dictionary of events at which the model should terminate - jac_rhs : :class:`pybamm.SparseStack` - Jacobian matrix for the differential equations - Raises ------ :class:`pybamm.SolverError` @@ -119,13 +64,16 @@ def set_up(self, model): should be used instead) """ + # Check for algebraic equations if len(model.algebraic) > 0: raise pybamm.SolverError( """Cannot use ODE solver to solve model with DAEs""" ) + # create simplified rhs and event expressions concatenated_rhs = model.concatenated_rhs events = model.events + if model.use_simplify: # set up simplification object, for re-use of dict simp = pybamm.Simplification() @@ -137,12 +85,13 @@ def set_up(self, model): events = {name: simp.simplify(event) for name, event in events.items()} y0 = model.concatenated_initial_conditions[:, 0] + if model.use_jacobian: # Create Jacobian from simplified rhs y = pybamm.StateVector(slice(0, np.size(y0))) - pybamm.logger.info("Calculating jacobian") jac_rhs = concatenated_rhs.jac(y) + if model.use_simplify: pybamm.logger.info("Simplifying jacobian") jac_rhs = simp.simplify(jac_rhs) @@ -150,7 +99,6 @@ def set_up(self, model): if model.use_to_python: pybamm.logger.info("Converting jacobian to python") jac_rhs = pybamm.EvaluatorPython(jac_rhs) - else: jac_rhs = None @@ -162,7 +110,37 @@ def set_up(self, model): name: pybamm.EvaluatorPython(event) for name, event in events.items() } - return concatenated_rhs, y0, events, jac_rhs + # Create function to evaluate rhs + def dydt(t, y): + pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) + y = y[:, np.newaxis] + dy = concatenated_rhs.evaluate(t, y, known_evals={})[0] + return dy[:, 0] + + # Create event-dependent function to evaluate events + def event_fun(event): + def eval_event(t, y): + return event.evaluate(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in events.values()] + + # Create function to evaluate jacobian + if jac_rhs is not None: + + def jacobian(t, y): + return jac_rhs.evaluate(t, y, known_evals={})[0] + + else: + jacobian = None + + # Add the solver attributes + self.y0 = y0 + self.dydt = dydt + self.events = events + self.event_funs = event_funs + self.jacobian = jacobian def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None diff --git a/results/2plus1D/set-temperature-spm-1plus1D.py b/results/2plus1D/set-temperature-spm-1plus1D.py index f7deea149c..84aef94a6f 100644 --- a/results/2plus1D/set-temperature-spm-1plus1D.py +++ b/results/2plus1D/set-temperature-spm-1plus1D.py @@ -3,14 +3,16 @@ import matplotlib.pyplot as plt import sys -plt.close('all') +plt.close("all") # set logging level pybamm.set_logging_level("INFO") # load (1+1D) SPM model -options = {"current collector": "jelly roll", - "dimensionality": 1, - "thermal": "set external temperature"} +options = { + "current collector": "jelly roll", + "dimensionality": 1, + "thermal": "set external temperature", +} model = pybamm.lithium_ion.SPM(options) # create geometry @@ -22,7 +24,7 @@ param.process_geometry(geometry) # set mesh -nbat = 108 +nbat = 2 var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: nbat} # depnding on number of points in y-z plane may need to increase recursion depth... @@ -33,9 +35,6 @@ disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) -t_sec = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge).evaluate() -t_hour = t_sec/(3600) - # define a method which updates statevector def update_statevector(variables, statevector): @@ -43,40 +42,33 @@ def update_statevector(variables, statevector): for name, new_vector in variables.items(): var_slice = model.variables[name].y_slices statevector[var_slice] = new_vector - return statevector[:, np.newaxis] # should be column vector - - -# define a method which takes a dimensional potential [V] and converts to the -# dimensionless potential used in pybamm -pot_scale = param.process_symbol( - pybamm.standard_parameters_lithium_ion.potential_scale -).evaluate() # potential scaled on thermal voltage -pot_ref = param.process_symbol( - pybamm.standard_parameters_lithium_ion.U_p_ref - - pybamm.standard_parameters_lithium_ion.U_n_ref -).evaluate() # positive potential measured with respect to reference OCV + return statevector -def non_dim_potential(phi_dim, domain): - if domain == "negative": - phi = phi_dim / pot_scale - elif domain == "positive": - phi = (phi_dim - pot_ref) / pot_scale - return phi - +# define a method to non-dimensionalise a temperature def non_dim_temperature(temperature): - Delta_T = param.process_symbol(model.submodels['thermal'].param.Delta_T).evaluate() - T_ref = param.process_symbol(model.submodels['thermal'].param.T_ref).evaluate() - return (temperature - T_ref)/Delta_T + "takes in a temperature and returns the non-dimensional version" + Delta_T = param.process_symbol(model.submodels["thermal"].param.Delta_T).evaluate() + T_ref = param.process_symbol(model.submodels["thermal"].param.T_ref).evaluate() + return (temperature - T_ref) / Delta_T + -# solve model -- replace with step -t_eval1 = np.linspace(0, 0.1, 20) -solution1 = model.default_solver.solve(model, t_eval1) +# step model in time +solver = model.default_solver +dt = 0.1 # timestep to take +npts = 20 # number of points to store the solution at during this step +solution1 = solver.step(model, dt, npts=npts) phi_s_cn_step1 = pybamm.ProcessedVariable( - model.variables["Negative current collector potential [V]"], solution1.t, solution1.y, mesh=mesh + model.variables["Negative current collector potential [V]"], + solution1.t, + solution1.y, + mesh=mesh, ) phi_s_cp_step1 = pybamm.ProcessedVariable( - model.variables["Positive current collector potential [V]"], solution1.t, solution1.y, mesh=mesh + model.variables["Positive current collector potential [V]"], + solution1.t, + solution1.y, + mesh=mesh, ) voltage_step1 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh @@ -85,45 +77,55 @@ def non_dim_temperature(temperature): model.variables["Current [A]"], solution1.t, solution1.y, mesh=mesh ) heating_step1 = pybamm.ProcessedVariable( - model.variables["X-averaged total heating [A.V.m-3]"], solution1.t, solution1.y, mesh=mesh + model.variables["X-averaged total heating [A.V.m-3]"], + solution1.t, + solution1.y, + mesh=mesh, ) particle_step1 = pybamm.ProcessedVariable( - model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution1.t, solution1.y, mesh=mesh + model.variables["X-averaged positive particle surface concentration [mol.m-3]"], + solution1.t, + solution1.y, + mesh=mesh, ) temperature_step1 = pybamm.ProcessedVariable( - model.variables["X-averaged cell temperature [K]"], solution1.t, solution1.y, mesh=mesh + model.variables["X-averaged cell temperature [K]"], + solution1.t, + solution1.y, + mesh=mesh, ) +# get the current state and temperature current_state = solution1.y[:, -1] - temp_ave = current_state[model.variables["X-averaged cell temperature"].y_slices] -temp_neg = current_state[model.variables["X-averaged negative electrode temperature"].y_slices] -temp_pos = current_state[model.variables["X-averaged positive electrode temperature"].y_slices] -temp_sep = current_state[model.variables["X-averaged separator temperature"].y_slices] -T_ref = param.process_symbol(model.submodels['thermal'].param.T_ref).evaluate() +# update the temperature +T_ref = param.process_symbol(model.submodels["thermal"].param.T_ref).evaluate() t_external = np.linspace(T_ref, T_ref + 6.0, nbat) non_dim_t_external = non_dim_temperature(t_external) -variables = { - "X-averaged cell temperature": non_dim_t_external, - "X-averaged negative electrode temperature": non_dim_t_external, - "X-averaged positive electrode temperature": non_dim_t_external, - "X-averaged separator temperature": non_dim_t_external, -} - +variables = {"X-averaged cell temperature": non_dim_t_external} new_state = update_statevector(variables, current_state) -# solve again -- replace with step -# use new state as initial condition -model.concatenated_initial_conditions = new_state -#model.concatenated_initial_conditions = current_state[:, np.newaxis] -t_eval2 = np.linspace(0.1, 0.2, 20) -solution2 = model.default_solver.solve(model, t_eval2) +# step in time again +# use new state as initial condition. Note: need to to recompute consistent initial +# values for the algebraic part of the model. Since the (dummy) equation for the +# temperature is an ODE, the imposed change in temperature is unaffected by this +# process +solver.y0 = solver.calculate_consistent_initial_conditions( + solver.rhs, solver.algebraic, new_state +) +solution2 = solver.step(model, dt, npts=npts) phi_s_cn_step2 = pybamm.ProcessedVariable( - model.variables["Negative current collector potential [V]"], solution2.t, solution2.y, mesh=mesh + model.variables["Negative current collector potential [V]"], + solution2.t, + solution2.y, + mesh=mesh, ) phi_s_cp_step2 = pybamm.ProcessedVariable( - model.variables["Positive current collector potential [V]"], solution2.t, solution2.y, mesh=mesh + model.variables["Positive current collector potential [V]"], + solution2.t, + solution2.y, + mesh=mesh, ) voltage_step2 = pybamm.ProcessedVariable( model.variables["Terminal voltage [V]"], solution2.t, solution2.y, mesh=mesh @@ -132,52 +134,93 @@ def non_dim_temperature(temperature): model.variables["Current [A]"], solution2.t, solution2.y, mesh=mesh ) heating_step2 = pybamm.ProcessedVariable( - model.variables["X-averaged total heating [A.V.m-3]"], solution2.t, solution2.y, mesh=mesh + model.variables["X-averaged total heating [A.V.m-3]"], + solution2.t, + solution2.y, + mesh=mesh, ) particle_step2 = pybamm.ProcessedVariable( - model.variables["X-averaged positive particle surface concentration [mol.m-3]"], solution2.t, solution2.y, mesh=mesh + model.variables["X-averaged positive particle surface concentration [mol.m-3]"], + solution2.t, + solution2.y, + mesh=mesh, ) temperature_step2 = pybamm.ProcessedVariable( - model.variables["X-averaged cell temperature [K]"], solution2.t, solution2.y, mesh=mesh + model.variables["X-averaged cell temperature [K]"], + solution2.t, + solution2.y, + mesh=mesh, ) + # plot +t_sec = param.process_symbol( + pybamm.standard_parameters_lithium_ion.tau_discharge +).evaluate() +t_hour = t_sec / (3600) plt.figure() z = np.linspace(0, 1, nbat) for bat_id in range(nbat): - plt.plot(t_eval1*t_hour, phi_s_cp_step1(t_eval1, z=z)[bat_id, :] - phi_s_cn_step1(t_eval1, z=z)[bat_id, :], t_eval2*t_hour, phi_s_cp_step2(t_eval2, z=z)[bat_id, :] - phi_s_cn_step2(t_eval2, z=z)[bat_id, :]) -plt.xlabel('t [hrs]') -plt.ylabel('Local voltage [V]') + plt.plot( + solution1.t * t_hour, + phi_s_cp_step1(solution1.t, z=z)[bat_id, :] + - phi_s_cn_step1(solution1.t, z=z)[bat_id, :], + solution2.t * t_hour, + phi_s_cp_step2(solution2.t, z=z)[bat_id, :] + - phi_s_cn_step2(solution2.t, z=z)[bat_id, :], + ) +plt.xlabel("t [hrs]") +plt.ylabel("Local voltage [V]") plt.figure() -plt.plot(t_eval1, voltage_step1(t_eval1), t_eval2, voltage_step2(t_eval2)) -plt.xlabel('t') -plt.ylabel('Voltage [V]') +plt.plot( + solution1.t, voltage_step1(solution1.t), solution2.t, voltage_step2(solution2.t) +) +plt.xlabel("t") +plt.ylabel("Voltage [V]") plt.show() plt.figure() -plt.plot(t_eval1, current_step1(t_eval1), t_eval2, current_step2(t_eval2)) -plt.xlabel('t') -plt.ylabel('Current [A]') +plt.plot( + solution1.t, current_step1(solution1.t), solution2.t, current_step2(solution2.t) +) +plt.xlabel("t") +plt.ylabel("Current [A]") plt.show() plt.figure() z = np.linspace(0, 1, nbat) for bat_id in range(nbat): - plt.plot(t_eval1*t_hour, heating_step1(t_eval1, z=z)[bat_id, :], t_eval2*t_hour, heating_step2(t_eval2, z=z)[bat_id, :]) -plt.xlabel('t [hrs]') -plt.ylabel('X-averaged total heating [A.V.m-3]') -plt.yscale('log') + plt.plot( + solution1.t * t_hour, + heating_step1(solution1.t, z=z)[bat_id, :], + solution2.t * t_hour, + heating_step2(solution2.t, z=z)[bat_id, :], + ) +plt.xlabel("t [hrs]") +plt.ylabel("X-averaged total heating [A.V.m-3]") +plt.yscale("log") plt.show() plt.figure() for bat_id in range(nbat): - plt.plot(t_eval1*t_hour, particle_step1(t_eval1,z=z)[bat_id, :], t_eval2*t_hour, particle_step2(t_eval2,z=z)[bat_id, :]) -plt.xlabel('t [hrs]') -plt.ylabel('X-averaged positive particle surface concentration [mol.m-3]') + plt.plot( + solution1.t * t_hour, + particle_step1(solution1.t, z=z)[bat_id, :], + solution2.t * t_hour, + particle_step2(solution2.t, z=z)[bat_id, :], + ) +plt.xlabel("t [hrs]") +plt.ylabel("X-averaged positive particle surface concentration [mol.m-3]") plt.show() plt.figure() for bat_id in range(nbat): - plt.plot(t_eval1*t_hour, temperature_step1(t_eval1,z=z)[bat_id, :], t_eval2*t_hour, temperature_step2(t_eval2,z=z)[bat_id, :]) -plt.xlabel('t [hrs]') -plt.ylabel('X-averaged cell temperature [K]') + plt.plot( + solution1.t * t_hour, + temperature_step1(solution1.t, z=z)[bat_id, :], + solution2.t * t_hour, + temperature_step2(solution2.t, z=z)[bat_id, :], + ) +plt.xlabel("t [hrs]") +plt.ylabel("X-averaged cell temperature [K]") plt.show() + def plot_var(var, solution, time=-1): variable = model.variables[var] len_x = len(mesh.combine_submeshes(*variable.domain)) @@ -196,11 +239,12 @@ def plot_var(var, solution, time=-1): plt.imshow(entries[:, :, time]) plt.title(var) -#plot_var(var="Positive current collector potential", solution=solution1, time=-1) -#plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) -#plot_var(var="Interfacial current density", solution=solution2, time=-1) -#plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) -#plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) + +# plot_var(var="Positive current collector potential", solution=solution1, time=-1) +# plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) +# plot_var(var="Interfacial current density", solution=solution2, time=-1) +# plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) +# plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) var_names = list(model.variables.keys()) var_names.sort() From 9865a89313421cbe1afa2b7192e297a74eb257c5 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 18 Sep 2019 10:47:10 +0100 Subject: [PATCH 017/122] address failed test NotImplementedError --- pybamm/solvers/base_solver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index c9fe1a8db1..2ced339279 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -46,6 +46,8 @@ def solve(self, model, t_eval): The times at which to compute the solution """ + if model is None: + raise NotImplementedError pybamm.logger.info("Start solving {}".format(model.name)) # Set up From 9b02bdab2ba0b70c898d15fca5639d5675ff9d14 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 18 Sep 2019 10:53:23 +0100 Subject: [PATCH 018/122] resolve conflict --- pybamm/solvers/ode_solver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index 6d4ae5c3d8..7d16dcb998 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -91,6 +91,7 @@ def set_up(self, model): y = pybamm.StateVector(slice(0, np.size(y0))) pybamm.logger.info("Calculating jacobian") jac_rhs = concatenated_rhs.jac(y) + model.jacobian = jac_rhs if model.use_simplify: pybamm.logger.info("Simplifying jacobian") From 8bcd3b77d928914762d12fee1281a5a9a4830cf8 Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 18 Sep 2019 11:46:32 +0100 Subject: [PATCH 019/122] trailing whitespace --- pybamm/models/submodels/current_collector/potential_pair.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index 7242ca1517..d6c5c69b09 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -192,5 +192,3 @@ def set_boundary_conditions(self, variables): "right": (pos_tab_bc, "Neumann"), }, } - - From 34faa9a8b7c9974b8bf3951d27e6f3c470165f01 Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 19 Sep 2019 10:19:54 +0100 Subject: [PATCH 020/122] add tests --- .../test_potential_pair.py | 3 ++ .../test_set_potential_spm_1plus1d.py | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py b/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py index b4112ad090..656182bdb5 100644 --- a/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py @@ -22,6 +22,9 @@ def test_public_functions(self): submodel = pybamm.current_collector.PotentialPair2plus1D(param) std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() + submodel = pybamm.current_collector.PotentialPairUnrolled(param) + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() if __name__ == "__main__": diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py new file mode 100644 index 0000000000..9c18a4871f --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py @@ -0,0 +1,39 @@ +# +# Test base current collector submodel +# + +import pybamm +import tests +import unittest +import pybamm.models.submodels.current_collector as cc + + +class TestSetPotetetialSPM1plus1DModel(unittest.TestCase): + def test_public_functions(self): + param = pybamm.standard_parameters_lithium_ion + submodel = cc.SetPotentialSingleParticle1plus1D(param) + val = pybamm.PrimaryBroadcast( + 0.0, "current collector" + ) + variables = { + "X-averaged positive electrode open circuit potential": val, + "X-averaged negative electrode open circuit potential": val, + "X-averaged positive electrode reaction overpotential": val, + "X-averaged negative electrode reaction overpotential": val, + "X-averaged electrolyte overpotential": val, + "X-averaged positive electrode ohmic losses": val, + "X-averaged negative electrode ohmic losses": val + } + std_tests = tests.StandardSubModelTests(submodel, variables) + + std_tests.test_all() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() From be22dc25c064f124c61153c7312fba971f7ff108 Mon Sep 17 00:00:00 2001 From: tom Date: Thu, 19 Sep 2019 12:21:07 +0100 Subject: [PATCH 021/122] style --- .../test_current_collector/test_set_potential_spm_1plus1d.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py index 9c18a4871f..4630fc8418 100644 --- a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py @@ -12,9 +12,7 @@ class TestSetPotetetialSPM1plus1DModel(unittest.TestCase): def test_public_functions(self): param = pybamm.standard_parameters_lithium_ion submodel = cc.SetPotentialSingleParticle1plus1D(param) - val = pybamm.PrimaryBroadcast( - 0.0, "current collector" - ) + val = pybamm.PrimaryBroadcast(0.0, "current collector") variables = { "X-averaged positive electrode open circuit potential": val, "X-averaged negative electrode open circuit potential": val, From 693919d5676e7957316421af06f0fb6c0dd59f9e Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 24 Sep 2019 16:23:07 +0100 Subject: [PATCH 022/122] #548 document options --- .../full_battery_models/base_battery_model.py | 22 ++++++++++++++----- .../current_collector/potential_pair.py | 7 ++++-- .../set_potential_single_particle.py | 14 ++++++++---- pybamm/solvers/base_solver.py | 6 +++-- ...plus1D.py => set_potential_spm_1plus1D.py} | 6 ++++- 5 files changed, 40 insertions(+), 15 deletions(-) rename results/2plus1D/{spm-1plus1D.py => set_potential_spm_1plus1D.py} (96%) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index deef0f335f..01de7594b9 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -38,15 +38,15 @@ class BaseBatteryModel(pybamm.BaseModel): (default) or "varying". Not currently implemented in any of the models. * "current collector" : str, optional Sets the current collector model to use. Can be "uniform" (default), - "potential pair", "potential pair quite conductive" or "single particle - potential pair". + "potential pair", "potential pair quite conductive", "single particle + potential pair", "jelly roll" or "set external potential". * "particle" : str, optional Sets the submodel to use to describe behaviour within the particle. Can be "Fickian diffusion" (default) or "fast diffusion". * "thermal" : str, optional Sets the thermal model to use. Can be "isothermal" (default), - "x-full", "x-lumped", "xyz-lumped" or "lumped". Must be "isothermal" for - lead-acid models. + "x-full", "x-lumped", "xyz-lumped", "lumped" or "set external + temperature". Must be "isothermal" for lead-acid models. * "thermal current collector" : bool, optional Whether to include thermal effects in the current collector in one-dimensional models (default is False). Note that this option @@ -592,9 +592,19 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.SetPotentialSingleParticle1plus1D( self.param ) + elif self.options["dimensionality"] in [0, 2]: + raise NotImplementedError( + """Set potential model only implemented for 1D current + collectors""" + ) elif self.options["current collector"] == "jelly roll": - submodel = pybamm.current_collector.PotentialPairUnrolled(self.param) - + if self.options["dimensionality"] == 1: + submodel = pybamm.current_collector.PotentialPairUnrolled(self.param) + elif self.options["dimensionality"] in [0, 2]: + raise NotImplementedError( + """Jelly roll model only implemented for 1D current + collectors""" + ) self.submodels["current collector"] = submodel def set_voltage_variables(self): diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index 6f65af4f47..0830696885 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -73,7 +73,7 @@ def set_initial_conditions(self, variables): class PotentialPair1plus1D(BasePotentialPair): - "Base class for a 1+1D potential pair model. Note: assumes both tabs at top" + "Base class for a 1+1D potential pair model." def __init__(self, param): super().__init__(param) @@ -177,7 +177,10 @@ def _get_effective_current_collector_area(self): class PotentialPairUnrolled(PotentialPair1plus1D): - "Base class for 1+1D model used to model an jelly roll with a tab at either end" + """ + Base class for 1+1D model used to model an 'unrolled' jelly roll with a tab + at either end + """ def set_boundary_conditions(self, variables): diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py index 0e84905a02..3d8ab6248c 100644 --- a/pybamm/models/submodels/current_collector/set_potential_single_particle.py +++ b/pybamm/models/submodels/current_collector/set_potential_single_particle.py @@ -6,13 +6,20 @@ class SetPotentialSingleParticle1plus1D(pybamm.BaseSubModel): """A submodel 1D current collectors which *doesn't* update the potentials - during solve. + during solve. This class uses the current-voltage relationship from the + SPM(e) (see [1]_) to calculate the current. Parameters ---------- param : parameter class The parameters to use for this submodel + References + ---------- + .. [1] SG Marquis, V Sulzer, R Timms, CP Please and SJ Chapman. “An asymptotic + derivation of a single particle model with electrolyte”. In: arXiv preprint + arXiv:1905.12553 (2019). + **Extends:** :class:`pybamm.current_collector.BaseModel` """ @@ -41,9 +48,8 @@ def _get_standard_potential_variables(self, phi_s_cn, phi_s_cp): # Local potential difference V_cc = phi_s_cp - phi_s_cn - # In 2D left corresponds to the negative tab and right the positive tab - phi_neg_tab = pybamm.BoundaryValue(phi_s_cn, "left") - phi_pos_tab = pybamm.BoundaryValue(phi_s_cp, "right") + phi_neg_tab = pybamm.BoundaryValue(phi_s_cn, "negative tab") + phi_pos_tab = pybamm.BoundaryValue(phi_s_cp, "positive tab") variables = { "Negative current collector potential": phi_s_cn, diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 3e88f45be4..a30216f13b 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -35,7 +35,8 @@ def tol(self, value): self._tol = value def solve(self, model, t_eval): - """Execute the solver setup and calculate the solution of the model at + """ + Execute the solver setup and calculate the solution of the model at specified times. Parameters @@ -74,7 +75,8 @@ def solve(self, model, t_eval): return solution def step(self, model, dt, npts=2): - """Step the solution of the model forward by a given time increment. The + """ + Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by calling `self.set_up(model)`. diff --git a/results/2plus1D/spm-1plus1D.py b/results/2plus1D/set_potential_spm_1plus1D.py similarity index 96% rename from results/2plus1D/spm-1plus1D.py rename to results/2plus1D/set_potential_spm_1plus1D.py index 5a833fd2d9..458a579fa3 100644 --- a/results/2plus1D/spm-1plus1D.py +++ b/results/2plus1D/set_potential_spm_1plus1D.py @@ -1,3 +1,7 @@ +# +# Example of 1+1D SPM where the potenital can be set by the user +# + import pybamm import numpy as np import sys @@ -7,7 +11,7 @@ # set logging level pybamm.set_logging_level("INFO") -# load (2+1D) SPM model +# load (1+1D) SPM model options = {"current collector": "set external potential", "dimensionality": 1} model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() From aa00a917270e0067f531deb101117f47b12c4341 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 24 Sep 2019 16:55:45 +0100 Subject: [PATCH 023/122] #548 add 1plus1D results --- .../base_current_collector.py | 2 + results/2plus1D/spm_1plus1D.py | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 results/2plus1D/spm_1plus1D.py diff --git a/pybamm/models/submodels/current_collector/base_current_collector.py b/pybamm/models/submodels/current_collector/base_current_collector.py index 219fa91c24..4b48dba561 100644 --- a/pybamm/models/submodels/current_collector/base_current_collector.py +++ b/pybamm/models/submodels/current_collector/base_current_collector.py @@ -78,6 +78,8 @@ def _get_standard_potential_variables(self, phi_s_cn, phi_s_cp): variables = { "Positive current collector potential": phi_s_cp, "Positive current collector potential [V]": U_ref + phi_s_cp * pot_scale, + "Local potenital difference": phi_s_cp - phi_s_cn, + "Local potenital difference [V]": U_ref + (phi_s_cp - phi_s_cn) * pot_scale, } variables.update(self._get_standard_negative_potential_variables(phi_s_cn)) diff --git a/results/2plus1D/spm_1plus1D.py b/results/2plus1D/spm_1plus1D.py new file mode 100644 index 0000000000..3bf9488682 --- /dev/null +++ b/results/2plus1D/spm_1plus1D.py @@ -0,0 +1,63 @@ +import pybamm +import numpy as np +import sys + +# set logging level +pybamm.set_logging_level("INFO") + +# load (1+1D) SPMe model +options = { + "current collector": "jelly roll", + "dimensionality": 1, + "thermal": "lumped", +} +model = pybamm.lithium_ion.SPM(options) + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry +param = model.default_parameter_values +C_rate = 1 +current_1C = 24 * param.process_symbol(pybamm.geometric_parameters.A_cc).evaluate() +param.update( + { + "Typical current [A]": C_rate * current_1C, + "Initial temperature [K]": 298.15, + "Negative current collector conductivity [S.m-1]": 1e5, + "Positive current collector conductivity [S.m-1]": 1e5, + "Heat transfer coefficient [W.m-2.K-1]": 1, + } +) +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: 15} +# depnding on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, model.default_spatial_methods) +disc.process_model(model) + +# solve model -- simulate one hour discharge +tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) +t_end = 3600 / tau.evaluate(0) +t_eval = np.linspace(0, t_end, 120) +solution = model.default_solver.solve(model, t_eval) + +# plot +output_variables = [ + "X-averaged negative particle surface concentration [mol.m-3]", + "X-averaged positive particle surface concentration [mol.m-3]", + #"X-averaged cell temperature [K]", + "Local potenital difference [V]", + "Current collector current density [A.m-2]", + "Terminal voltage [V]", + "Volume-averaged cell temperature [K]", +] +plot = pybamm.QuickPlot(model, mesh, solution, output_variables) +plot.dynamic_plot() From 925beb184972a10bf9ee3d73088e4c63f3e395a4 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 25 Sep 2019 17:16:38 +0100 Subject: [PATCH 024/122] #548 start adding user supplied mesh --- pybamm/meshes/meshes.py | 6 +- pybamm/meshes/one_dimensional_submeshes.py | 71 ++++++++++++++++++++-- results/2plus1D/user_mesh_spm_1plus1D.py | 66 ++++++++++++++++++++ 3 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 results/2plus1D/user_mesh_spm_1plus1D.py diff --git a/pybamm/meshes/meshes.py b/pybamm/meshes/meshes.py index e4f79ff331..3eb7a7317b 100644 --- a/pybamm/meshes/meshes.py +++ b/pybamm/meshes/meshes.py @@ -32,7 +32,7 @@ def __init__(self, geometry, submesh_types, var_pts): submesh_pts = {} for domain in geometry: # Zero dimensional submesh case (only one point) - if issubclass(submesh_types[domain], pybamm.SubMesh0D): + if submesh_types[domain] == pybamm.SubMesh0D: submesh_pts[domain] = 1 # other cases else: @@ -95,8 +95,6 @@ def __init__(self, geometry, submesh_types, var_pts): # Create submeshes for domain in geometry: - # need to pass tab information if primary domain is 1 or 2D - # current collector if ( domain == "current collector" and submesh_types[domain] != pybamm.SubMesh0D @@ -176,7 +174,7 @@ def add_ghost_meshes(self): (domain, submesh_list) for domain, submesh_list in self.items() if not isinstance( - submesh_list[0], (pybamm.SubMesh0D, pybamm.Scikit2DSubMesh) + submesh_list[0], (pybamm.SubMesh0D, pybamm.ScikitSubMesh2D) ) ] for domain, submesh_list in submeshes: diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/pybamm/meshes/one_dimensional_submeshes.py index e269064572..82aa7fb4b2 100644 --- a/pybamm/meshes/one_dimensional_submeshes.py +++ b/pybamm/meshes/one_dimensional_submeshes.py @@ -46,7 +46,8 @@ class Uniform1DSubMesh(SubMesh1D): A dictionary that contains the limits of the spatial variables npts : dict A dictionary that contains the number of points to be used on each - spatial variable + spatial variable. Note: the number of nodes (located at the cell centres) + is npts, and the number of edges is npts+1. tabs : dict, optional A dictionary that contains information about the size and location of the tabs @@ -54,9 +55,6 @@ class Uniform1DSubMesh(SubMesh1D): def __init__(self, lims, npts, tabs=None): - # currently accept lims and npts as dicts. This may get changed at a future - # date depending on the form of mesh we desire for 2D/3D - # check that only one variable passed in if len(lims) != 1: raise pybamm.GeometryError("lims should only contain a single variable") @@ -70,3 +68,68 @@ def __init__(self, lims, npts, tabs=None): coord_sys = spatial_var.coord_sys super().__init__(edges, coord_sys=coord_sys, tabs=tabs) + + +class GetUserSupplied1DSubMesh: + """ + A class to generate a submesh on a 1D domain using a user supplied vector of + nodes. + + Parameters + ---------- + edges : array_like + The array of points which correspond to the edges of the mesh. + + + """ + + def __init__(self, nodes): + self.nodes = nodes + + def __call__(self, lims, npts, tabs=None): + return UserSupplied1DSubMesh(lims, npts, tabs, self.nodes) + + +class UserSupplied1DSubMesh(SubMesh1D): + """ + A class to generate a submesh on a 1D domain from a user supplied array of + nodes. + + Parameters + ---------- + lims : dict + A dictionary that contains the limits of the spatial variables + npts : dict + A dictionary that contains the number of points to be used on each + spatial variable. Note: the number of nodes (located at the cell centres) + is npts, and the number of edges is npts+1. + tabs : dict + A dictionary that contains information about the size and location of + the tabs + edges : array_like + The array of points which correspond to the edges of the mesh. + """ + + def __init__(self, lims, npts, tabs, edges): + + # check that only one variable passed in + if len(lims) != 1: + raise pybamm.GeometryError("lims should only contain a single variable") + + spatial_var = list(lims.keys())[0] + spatial_lims = lims[spatial_var] + npts = npts[spatial_var.id] + + # check that npts + 1 equals number of user-supplied edges + if (npts + 1) != len(edges): + raise pybamm.GeometryError("Number of points for") + + # check end points of edges agrees with spatial_lims + if edges[0] != spatial_lims["min"]: + raise pybamm.GeometryError() + if edges[-1] != spatial_lims["max"]: + raise pybamm.GeometryError() + + coord_sys = spatial_var.coord_sys + + super().__init__(edges, coord_sys=coord_sys, tabs=tabs) diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py new file mode 100644 index 0000000000..8fbfbfc50d --- /dev/null +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -0,0 +1,66 @@ +import pybamm +import numpy as np +import sys + +# set logging level +pybamm.set_logging_level("INFO") + +# load (1+1D) SPMe model +options = { + "current collector": "jelly roll", + "dimensionality": 1, + "thermal": "lumped", +} +model = pybamm.lithium_ion.SPM(options) + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry +param = model.default_parameter_values +C_rate = 1 +current_1C = 24 * param.process_symbol(pybamm.geometric_parameters.A_cc).evaluate() +param.update( + { + "Typical current [A]": C_rate * current_1C, + "Initial temperature [K]": 298.15, + "Negative current collector conductivity [S.m-1]": 1e5, + "Positive current collector conductivity [S.m-1]": 1e5, + "Heat transfer coefficient [W.m-2.K-1]": 1, + } +) +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: 15} +# depnding on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) +submesh_types = model.default_submesh_types +z = np.array([0, 0.1, 0.3, 0.5, 0.8, 1]) +submesh_types["current collector"] = pybamm.GetUserSupplied1DSubMesh(z) +mesh = pybamm.Mesh(geometry, submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, model.defualt_spatial_methods) +disc.process_model(model) + +# solve model -- simulate one hour discharge +tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) +t_end = 3600 / tau.evaluate(0) +t_eval = np.linspace(0, t_end, 120) +solution = model.default_solver.solve(model, t_eval) + +# plot +output_variables = [ + "X-averaged negative particle surface concentration [mol.m-3]", + "X-averaged positive particle surface concentration [mol.m-3]", + #"X-averaged cell temperature [K]", + "Local potenital difference [V]", + "Current collector current density [A.m-2]", + "Terminal voltage [V]", + "Volume-averaged cell temperature [K]", +] +plot = pybamm.QuickPlot(model, mesh, solution, output_variables) +plot.dynamic_plot() From 327fa0b4690a07d226e7b9f182269591caa4269e Mon Sep 17 00:00:00 2001 From: tom Date: Wed, 25 Sep 2019 17:29:47 +0100 Subject: [PATCH 025/122] output more x-averaged heating variables --- .../models/submodels/thermal/base_thermal.py | 28 +++++++++++++++---- results/2plus1D/spm_1plus1D.py | 4 +-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index 7cc87dc616..f158382e4a 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -119,11 +119,14 @@ def _get_standard_coupled_variables(self, variables): ) Q = Q_ohm + Q_rxn + Q_rev - Q_av = pybamm.x_average(Q) +# Q_av = pybamm.x_average(Q) # Compute the x-average over the current collectors. - Q_av = self._x_average(Q, Q_ohm_s_cn, Q_ohm_s_cp) - Q_vol_av = self._yz_average(Q_av) + Q_av_ohm = self._x_average(Q_ohm, Q_ohm_s_cn, Q_ohm_s_cp) + Q_av_rxn = pybamm.x_average(Q_rxn) + Q_av_rev = pybamm.x_average(Q_rev) + Q_av_tot = self._x_average(Q, Q_ohm_s_cn, Q_ohm_s_cp) + Q_vol_av = self._yz_average(Q_av_tot) variables.update( { @@ -147,10 +150,25 @@ def _get_standard_coupled_variables(self, variables): * param.potential_scale * Q / param.L_x, - "X-averaged total heating": Q_av, + "X-averaged Ohmic heating": Q_av_ohm, + "X-averaged Ohmic heating [A.V.m-3]": param.i_typ + * param.potential_scale + * Q_av_ohm + / param.L_x, + "X-averaged irreversible electrochemical heating": Q_av_rxn, + "X-averaged irreversible electrochemical heating [A.V.m-3]": param.i_typ + * param.potential_scale + * Q_av_rxn + / param.L_x, + "X-averaged reversible heating": Q_av_rev, + "X-averaged reversible heating [A.V.m-3]": param.i_typ + * param.potential_scale + * Q_av_rev + / param.L_x, + "X-averaged total heating": Q_av_tot, "X-averaged total heating [A.V.m-3]": param.i_typ * param.potential_scale - * Q_av + * Q_av_tot / param.L_x, "Volume-averaged total heating": Q_vol_av, "Volume-averaged total heating [A.V.m-3]": param.i_typ diff --git a/results/2plus1D/spm_1plus1D.py b/results/2plus1D/spm_1plus1D.py index 3bf9488682..cbaefdf836 100644 --- a/results/2plus1D/spm_1plus1D.py +++ b/results/2plus1D/spm_1plus1D.py @@ -24,8 +24,8 @@ { "Typical current [A]": C_rate * current_1C, "Initial temperature [K]": 298.15, - "Negative current collector conductivity [S.m-1]": 1e5, - "Positive current collector conductivity [S.m-1]": 1e5, + "Negative current collector conductivity [S.m-1]": 1e7, + "Positive current collector conductivity [S.m-1]": 1e7, "Heat transfer coefficient [W.m-2.K-1]": 1, } ) From 9cf163ae87d93b40b43718d5ee983e2cc192ef77 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 25 Sep 2019 17:53:16 +0100 Subject: [PATCH 026/122] #548 raise errors for incompatible user mesh --- pybamm/__init__.py | 6 +++++- pybamm/meshes/one_dimensional_submeshes.py | 21 ++++++++++++++++++--- results/2plus1D/user_mesh_spm_1plus1D.py | 17 +++++++---------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/pybamm/__init__.py b/pybamm/__init__.py index ca8d2e761d..0f2fd8f01f 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -213,7 +213,11 @@ def version(formatted=False): from .discretisations.discretisation import Discretisation from .meshes.meshes import Mesh from .meshes.zero_dimensional_submesh import SubMesh0D -from .meshes.one_dimensional_submeshes import SubMesh1D, Uniform1DSubMesh +from .meshes.one_dimensional_submeshes import ( + SubMesh1D, + Uniform1DSubMesh, + GetUserSupplied1DSubMesh, +) from .meshes.scikit_fem_submeshes import Scikit2DSubMesh # diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/pybamm/meshes/one_dimensional_submeshes.py index 82aa7fb4b2..8e4f3dda24 100644 --- a/pybamm/meshes/one_dimensional_submeshes.py +++ b/pybamm/meshes/one_dimensional_submeshes.py @@ -119,16 +119,31 @@ def __init__(self, lims, npts, tabs, edges): spatial_var = list(lims.keys())[0] spatial_lims = lims[spatial_var] npts = npts[spatial_var.id] + import ipdb + ipdb.set_trace() # check that npts + 1 equals number of user-supplied edges if (npts + 1) != len(edges): - raise pybamm.GeometryError("Number of points for") + raise pybamm.GeometryError( + """User-suppled edges has should have length (npts + 1) but has length {}. + Number of points (npts) for domain {} is {}.""".format( + len(edges), spatial_var.domain, npts + ) + ) # check end points of edges agrees with spatial_lims if edges[0] != spatial_lims["min"]: - raise pybamm.GeometryError() + raise pybamm.GeometryError( + "First entry of edges is , but should be equal to {} for domain {}.".format( + edges[0], spatial_lims["min"], spatial_var.domain + ) + ) if edges[-1] != spatial_lims["max"]: - raise pybamm.GeometryError() + raise pybamm.GeometryError( + "Last entry of edges is , but should be equal to {} for domain {}.".format( + edges[-1], spatial_lims["max"], spatial_var.domain + ) + ) coord_sys = spatial_var.coord_sys diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py index 8fbfbfc50d..38c6d68283 100644 --- a/results/2plus1D/user_mesh_spm_1plus1D.py +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -6,11 +6,7 @@ pybamm.set_logging_level("INFO") # load (1+1D) SPMe model -options = { - "current collector": "jelly roll", - "dimensionality": 1, - "thermal": "lumped", -} +options = {"current collector": "jelly roll", "dimensionality": 1, "thermal": "lumped"} model = pybamm.lithium_ion.SPM(options) # create geometry @@ -33,13 +29,14 @@ param.process_geometry(geometry) # set mesh -var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: 15} -# depnding on number of points in y-z plane may need to increase recursion depth... -sys.setrecursionlimit(10000) submesh_types = model.default_submesh_types z = np.array([0, 0.1, 0.3, 0.5, 0.8, 1]) +npts_z = len(z) - 1 submesh_types["current collector"] = pybamm.GetUserSupplied1DSubMesh(z) +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: npts_z} +# depnding on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # discretise model @@ -56,7 +53,7 @@ output_variables = [ "X-averaged negative particle surface concentration [mol.m-3]", "X-averaged positive particle surface concentration [mol.m-3]", - #"X-averaged cell temperature [K]", + # "X-averaged cell temperature [K]", "Local potenital difference [V]", "Current collector current density [A.m-2]", "Terminal voltage [V]", From eb459b56e818bd269066fad9f52824d0924f53aa Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 25 Sep 2019 18:06:03 +0100 Subject: [PATCH 027/122] #548 fixes to meshes --- docs/source/meshes/scikits_fem_submeshes.rst | 2 +- pybamm/__init__.py | 2 +- pybamm/meshes/one_dimensional_submeshes.py | 8 +- pybamm/meshes/scikit_fem_submeshes.py | 136 +++++++++--------- .../full_battery_models/base_battery_model.py | 2 +- .../effective_resistance_current_collector.py | 2 +- pybamm/processed_variable.py | 2 +- results/2plus1D/user_mesh_spm_1plus1D.py | 12 +- tests/shared.py | 4 +- .../test_meshes/test_scikit_fem_submesh.py | 14 +- .../test_base_battery_model.py | 3 +- .../test_lead_acid/test_loqs.py | 3 +- 12 files changed, 100 insertions(+), 90 deletions(-) diff --git a/docs/source/meshes/scikits_fem_submeshes.rst b/docs/source/meshes/scikits_fem_submeshes.rst index 11195ba49c..5bebbff3db 100644 --- a/docs/source/meshes/scikits_fem_submeshes.rst +++ b/docs/source/meshes/scikits_fem_submeshes.rst @@ -1,5 +1,5 @@ Scikits FEM Submeshes ===================== -.. autoclass:: pybamm.Scikit2DSubMesh +.. autoclass:: pybamm.ScikitUniform2DSubMesh :members: diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 0f2fd8f01f..2262810d96 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -218,7 +218,7 @@ def version(formatted=False): Uniform1DSubMesh, GetUserSupplied1DSubMesh, ) -from .meshes.scikit_fem_submeshes import Scikit2DSubMesh +from .meshes.scikit_fem_submeshes import ScikitSubMesh2D, ScikitUniform2DSubMesh # # Spatial Methods diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/pybamm/meshes/one_dimensional_submeshes.py index 8e4f3dda24..2e675a30f1 100644 --- a/pybamm/meshes/one_dimensional_submeshes.py +++ b/pybamm/meshes/one_dimensional_submeshes.py @@ -119,9 +119,7 @@ def __init__(self, lims, npts, tabs, edges): spatial_var = list(lims.keys())[0] spatial_lims = lims[spatial_var] npts = npts[spatial_var.id] - import ipdb - ipdb.set_trace() # check that npts + 1 equals number of user-supplied edges if (npts + 1) != len(edges): raise pybamm.GeometryError( @@ -134,13 +132,15 @@ def __init__(self, lims, npts, tabs, edges): # check end points of edges agrees with spatial_lims if edges[0] != spatial_lims["min"]: raise pybamm.GeometryError( - "First entry of edges is , but should be equal to {} for domain {}.".format( + """First entry of edges is {}, but should be equal to {} + for domain {}.""".format( edges[0], spatial_lims["min"], spatial_var.domain ) ) if edges[-1] != spatial_lims["max"]: raise pybamm.GeometryError( - "Last entry of edges is , but should be equal to {} for domain {}.".format( + """Last entry of edges is {}, but should be equal to {} + for domain {}.""".format( edges[-1], spatial_lims["max"], spatial_var.domain ) ) diff --git a/pybamm/meshes/scikit_fem_submeshes.py b/pybamm/meshes/scikit_fem_submeshes.py index 0d86bb74e4..a6038bfbc8 100644 --- a/pybamm/meshes/scikit_fem_submeshes.py +++ b/pybamm/meshes/scikit_fem_submeshes.py @@ -4,74 +4,23 @@ import pybamm import numpy as np -import importlib - -skfem_spec = importlib.util.find_spec("skfem") -if skfem_spec is not None: - skfem = importlib.util.module_from_spec(skfem_spec) - skfem_spec.loader.exec_module(skfem) - - -class Scikit2DSubMesh: - """ Submesh class. - Contains information about the 2D finite element mesh. - Note: This class only allows for the use of piecewise-linear triangular - finite elements. - - Parameters - ---------- - lims : dict - A dictionary that contains the limits of each - spatial variable - npts : dict - A dictionary that contains the number of points to be used on each - spatial variable - tabs : dict - A dictionary that contains information about the size and location of - the tabs - """ +import skfem - def __init__(self, lims, npts, tabs): - if skfem_spec is None: - raise ImportError("scikit-fem is not installed") - # check that two variables have been passed in - if len(lims) != 2: - raise pybamm.GeometryError( - "lims should contain exactly two variables, not {}".format(len(lims)) - ) +class ScikitSubMesh2D: + """ + Contains information about the 2D finite element mesh. + Note: This class only allows for the use of piecewise-linear triangular + finite elements. + """ - # get spatial variables - spatial_vars = list(lims.keys()) - - # check coordinate system agrees - if spatial_vars[0].coord_sys == spatial_vars[1].coord_sys: - self.coord_sys = spatial_vars[0].coord_sys - else: - raise pybamm.DomainError( - """spatial variables should have the same coordinate system, - but have coordinate systems {} and {}""".format( - spatial_vars[0].coord_sys, spatial_vars[1].coord_sys - ) - ) - - # set limits and number of points - self.npts = 1 - self.edges = {} - self.nodes = {} - for var in spatial_vars: - if var.name not in ["y", "z"]: - raise pybamm.DomainError( - "spatial variable must be y or z not {}".format(var.name) - ) - else: - self.npts *= npts[var.id] - self.edges[var.name] = np.linspace( - lims[var]["min"], lims[var]["max"], npts[var.id] - ) - self.nodes[var.name] = ( - self.edges[var.name][1:] + self.edges[var.name][:-1] - ) / 2 + def __init__(self, edges, coord_sys, tabs): + self.edges = edges + self.nodes = dict.fromkeys(["y", "z"]) + for var in self.nodes.keys(): + self.nodes[var] = (self.edges[var][1:] + self.edges[var][:-1]) / 2 + self.npts = len(self.edges["y"]) * len(self.edges["z"]) + self.coord_sys = coord_sys # create mesh self.fem_mesh = skfem.MeshTri.init_tensor(self.edges["y"], self.edges["z"]) @@ -150,3 +99,60 @@ def between(x, interval, tol=3e-16): ] else: raise pybamm.GeometryError("tab location not valid") + + +class ScikitUniform2DSubMesh(ScikitSubMesh2D): + """ + Contains information about the 2D finite element mesh with uniform grid + spacing (can be different spacing in y and z). + Note: This class only allows for the use of piecewise-linear triangular + finite elements. + + Parameters + ---------- + lims : dict + A dictionary that contains the limits of each + spatial variable + npts : dict + A dictionary that contains the number of points to be used on each + spatial variable + tabs : dict + A dictionary that contains information about the size and location of + the tabs + """ + + def __init__(self, lims, npts, tabs): + + # check that two variables have been passed in + if len(lims) != 2: + raise pybamm.GeometryError( + "lims should contain exactly two variables, not {}".format(len(lims)) + ) + + # get spatial variables + spatial_vars = list(lims.keys()) + + # check coordinate system agrees + if spatial_vars[0].coord_sys == spatial_vars[1].coord_sys: + coord_sys = spatial_vars[0].coord_sys + else: + raise pybamm.DomainError( + """spatial variables should have the same coordinate system, + but have coordinate systems {} and {}""".format( + spatial_vars[0].coord_sys, spatial_vars[1].coord_sys + ) + ) + + # compute edges + edges = {} + for var in spatial_vars: + if var.name not in ["y", "z"]: + raise pybamm.DomainError( + "spatial variable must be y or z not {}".format(var.name) + ) + else: + edges[var.name] = np.linspace( + lims[var]["min"], lims[var]["max"], npts[var.id] + ) + + super().__init__(edges, coord_sys, tabs) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 01de7594b9..c2887fb975 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -150,7 +150,7 @@ def default_submesh_types(self): elif self.options["dimensionality"] == 1: base_submeshes["current collector"] = pybamm.Uniform1DSubMesh elif self.options["dimensionality"] == 2: - base_submeshes["current collector"] = pybamm.Scikit2DSubMesh + base_submeshes["current collector"] = pybamm.ScikitUniform2DSubMesh return base_submeshes @property diff --git a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py index 87f234939e..4592c451f1 100644 --- a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py +++ b/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py @@ -232,7 +232,7 @@ def default_var_pts(self): @property def default_submesh_types(self): - return {"current collector": pybamm.Scikit2DSubMesh} + return {"current collector": pybamm.ScikitUniform2DSubMesh} @property def default_spatial_methods(self): diff --git a/pybamm/processed_variable.py b/pybamm/processed_variable.py index c7842ea10d..f6ba662a44 100644 --- a/pybamm/processed_variable.py +++ b/pybamm/processed_variable.py @@ -95,7 +95,7 @@ def __init__( # handle 2D (in space) finite element variables differently if mesh and "current collector" in self.domain and isinstance( - self.mesh[self.domain[0]][0], pybamm.Scikit2DSubMesh + self.mesh[self.domain[0]][0], pybamm.ScikitUniform2DSubMesh ): if len(self.t_sol) == 1: # space only (steady solution) diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py index 38c6d68283..ae8272a058 100644 --- a/results/2plus1D/user_mesh_spm_1plus1D.py +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -28,11 +28,13 @@ param.process_model(model) param.process_geometry(geometry) -# set mesh +# set mesh using user-supplied edges in z +z_edges = np.array([0, 0.03, 0.1, 0.3, 0.47, 0.5, 0.73, 0.8, 0.911, 1]) submesh_types = model.default_submesh_types -z = np.array([0, 0.1, 0.3, 0.5, 0.8, 1]) -npts_z = len(z) - 1 -submesh_types["current collector"] = pybamm.GetUserSupplied1DSubMesh(z) +submesh_types["current collector"] = pybamm.GetUserSupplied1DSubMesh(z_edges) +# Need to make sure var_pts for z is ones less than number of edges (variables are +# evaluated at cell centres) +npts_z = len(z_edges) - 1 var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: npts_z} # depnding on number of points in y-z plane may need to increase recursion depth... @@ -40,7 +42,7 @@ mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # discretise model -disc = pybamm.Discretisation(mesh, model.defualt_spatial_methods) +disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) # solve model -- simulate one hour discharge diff --git a/tests/shared.py b/tests/shared.py index 1e55ce4d4c..23e77c9dfa 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -103,7 +103,7 @@ def get_1p1d_mesh_for_testing(xpts=None, zpts=15, cc_submesh=pybamm.Uniform1DSub def get_2p1d_mesh_for_testing( - xpts=None, ypts=15, zpts=15, cc_submesh=pybamm.Scikit2DSubMesh + xpts=None, ypts=15, zpts=15, cc_submesh=pybamm.ScikitUniform2DSubMesh ): geometry = pybamm.Geometry("2+1D macro") return get_mesh_for_testing( @@ -138,7 +138,7 @@ def get_unit_2p1D_mesh_for_testing(ypts=15, zpts=15): "negative electrode": pybamm.Uniform1DSubMesh, "separator": pybamm.Uniform1DSubMesh, "positive electrode": pybamm.Uniform1DSubMesh, - "current collector": pybamm.Scikit2DSubMesh, + "current collector": pybamm.ScikitUniform2DSubMesh, } return pybamm.Mesh(geometry, submesh_types, var_pts) diff --git a/tests/unit/test_meshes/test_scikit_fem_submesh.py b/tests/unit/test_meshes/test_scikit_fem_submesh.py index 4432424329..0858359db1 100644 --- a/tests/unit/test_meshes/test_scikit_fem_submesh.py +++ b/tests/unit/test_meshes/test_scikit_fem_submesh.py @@ -33,7 +33,7 @@ def test_mesh_creation(self): "negative electrode": pybamm.Uniform1DSubMesh, "separator": pybamm.Uniform1DSubMesh, "positive electrode": pybamm.Uniform1DSubMesh, - "current collector": pybamm.Scikit2DSubMesh, + "current collector": pybamm.ScikitUniform2DSubMesh, } mesh_type = pybamm.Mesh @@ -67,7 +67,7 @@ def test_init_failure(self): "negative electrode": pybamm.Uniform1DSubMesh, "separator": pybamm.Uniform1DSubMesh, "positive electrode": pybamm.Uniform1DSubMesh, - "current collector": pybamm.Scikit2DSubMesh, + "current collector": pybamm.ScikitUniform2DSubMesh, } geometry = pybamm.Geometryxp1DMacro(cc_dimension=2) with self.assertRaises(KeyError): @@ -81,14 +81,14 @@ def test_init_failure(self): lims = {var.x_n: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} with self.assertRaises(pybamm.GeometryError): - pybamm.Scikit2DSubMesh(lims, None, None) + pybamm.ScikitUniform2DSubMesh(lims, None, None) lims = { var.x_n: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, var.x_p: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } with self.assertRaises(pybamm.DomainError): - pybamm.Scikit2DSubMesh(lims, None, None) + pybamm.ScikitUniform2DSubMesh(lims, None, None) lims = { var.y: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, @@ -97,7 +97,7 @@ def test_init_failure(self): npts = {var.y.id: 10, var.z.id: 10} var.z.coord_sys = "not cartesian" with self.assertRaises(pybamm.DomainError): - pybamm.Scikit2DSubMesh(lims, npts, None) + pybamm.ScikitUniform2DSubMesh(lims, npts, None) var.z.coord_sys = "cartesian" def test_tab_error(self): @@ -109,7 +109,7 @@ def test_tab_error(self): "negative electrode": pybamm.Uniform1DSubMesh, "separator": pybamm.Uniform1DSubMesh, "positive electrode": pybamm.Uniform1DSubMesh, - "current collector": pybamm.Scikit2DSubMesh, + "current collector": pybamm.ScikitUniform2DSubMesh, } mesh_type = pybamm.Mesh @@ -146,7 +146,7 @@ def test_tab_left_right(self): "negative electrode": pybamm.Uniform1DSubMesh, "separator": pybamm.Uniform1DSubMesh, "positive electrode": pybamm.Uniform1DSubMesh, - "current collector": pybamm.Scikit2DSubMesh, + "current collector": pybamm.ScikitUniform2DSubMesh, } mesh_type = pybamm.Mesh diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index f9726fefa5..d318975e4e 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -56,7 +56,8 @@ def test_default_submesh_types(self): model = pybamm.BaseBatteryModel({"dimensionality": 2}) self.assertTrue( issubclass( - model.default_submesh_types["current collector"], pybamm.Scikit2DSubMesh + model.default_submesh_types["current collector"], + pybamm.ScikitUniform2DSubMesh, ) ) diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index c5c44508cc..01ae5c0ca3 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -88,7 +88,8 @@ def test_defaults_dimensions(self): ) self.assertTrue( issubclass( - model.default_submesh_types["current collector"], pybamm.Scikit2DSubMesh + model.default_submesh_types["current collector"], + pybamm.ScikitUniform2DSubMesh, ) ) From bd81af74ba3b531a90420fdfe9b878750ac5c779 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 25 Sep 2019 18:09:43 +0100 Subject: [PATCH 028/122] #548 fix typos --- pybamm/meshes/one_dimensional_submeshes.py | 2 +- results/2plus1D/dfn_2plus1D.py | 2 +- results/2plus1D/set-temperature-spm-1plus1D.py | 2 +- results/2plus1D/set_potential_spm_1plus1D.py | 2 +- results/2plus1D/spm_1plus1D.py | 2 +- results/2plus1D/spm_2plus1D.py | 2 +- results/2plus1D/spme_2plus1D.py | 2 +- results/2plus1D/user_mesh_spm_1plus1D.py | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/pybamm/meshes/one_dimensional_submeshes.py index 2e675a30f1..c5eae37116 100644 --- a/pybamm/meshes/one_dimensional_submeshes.py +++ b/pybamm/meshes/one_dimensional_submeshes.py @@ -129,7 +129,7 @@ def __init__(self, lims, npts, tabs, edges): ) ) - # check end points of edges agrees with spatial_lims + # check end points of edges agree with spatial_lims if edges[0] != spatial_lims["min"]: raise pybamm.GeometryError( """First entry of edges is {}, but should be equal to {} diff --git a/results/2plus1D/dfn_2plus1D.py b/results/2plus1D/dfn_2plus1D.py index f39e70ab0a..2c87764970 100644 --- a/results/2plus1D/dfn_2plus1D.py +++ b/results/2plus1D/dfn_2plus1D.py @@ -39,7 +39,7 @@ var.y: 5, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) diff --git a/results/2plus1D/set-temperature-spm-1plus1D.py b/results/2plus1D/set-temperature-spm-1plus1D.py index 84aef94a6f..4eafcdf583 100644 --- a/results/2plus1D/set-temperature-spm-1plus1D.py +++ b/results/2plus1D/set-temperature-spm-1plus1D.py @@ -27,7 +27,7 @@ nbat = 2 var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: nbat} -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) diff --git a/results/2plus1D/set_potential_spm_1plus1D.py b/results/2plus1D/set_potential_spm_1plus1D.py index 458a579fa3..b9d8ecff4a 100644 --- a/results/2plus1D/set_potential_spm_1plus1D.py +++ b/results/2plus1D/set_potential_spm_1plus1D.py @@ -35,7 +35,7 @@ var.y: 1, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) diff --git a/results/2plus1D/spm_1plus1D.py b/results/2plus1D/spm_1plus1D.py index 3bf9488682..6dbf30fc42 100644 --- a/results/2plus1D/spm_1plus1D.py +++ b/results/2plus1D/spm_1plus1D.py @@ -35,7 +35,7 @@ # set mesh var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: 15} -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) diff --git a/results/2plus1D/spm_2plus1D.py b/results/2plus1D/spm_2plus1D.py index 3c3603fdc0..031a75c84c 100644 --- a/results/2plus1D/spm_2plus1D.py +++ b/results/2plus1D/spm_2plus1D.py @@ -39,7 +39,7 @@ var.y: 5, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) diff --git a/results/2plus1D/spme_2plus1D.py b/results/2plus1D/spme_2plus1D.py index c516e26c30..385bc059d2 100644 --- a/results/2plus1D/spme_2plus1D.py +++ b/results/2plus1D/spme_2plus1D.py @@ -39,7 +39,7 @@ var.y: 5, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py index ae8272a058..997a8ecaaa 100644 --- a/results/2plus1D/user_mesh_spm_1plus1D.py +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -32,12 +32,12 @@ z_edges = np.array([0, 0.03, 0.1, 0.3, 0.47, 0.5, 0.73, 0.8, 0.911, 1]) submesh_types = model.default_submesh_types submesh_types["current collector"] = pybamm.GetUserSupplied1DSubMesh(z_edges) -# Need to make sure var_pts for z is ones less than number of edges (variables are +# Need to make sure var_pts for z is one less than number of edges (variables are # evaluated at cell centres) npts_z = len(z_edges) - 1 var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: npts_z} -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, submesh_types, var_pts) From 8958f322c288b3da16964a7878648002fead40ab Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Thu, 26 Sep 2019 17:59:57 +0100 Subject: [PATCH 029/122] #548 docs and tests --- .../submodels/current_collector/index.rst | 3 +- .../set_potential_single_particle.rst | 5 ++ .../x_lumped/x_lumped_1D_set_temperature.rst | 4 +- pybamm/meshes/one_dimensional_submeshes.py | 12 +++- .../full_battery_models/base_battery_model.py | 28 +++++++- .../set_potential_single_particle.py | 7 ++ .../submodels/electrode/base_electrode.py | 4 +- .../submodels/electrode/ohm/composite_ohm.py | 1 - .../submodels/electrode/ohm/leading_ohm.py | 4 +- ...us1D.py => set_temperature_spm_1plus1D.py} | 0 tests/unit/test_meshes/test_meshes.py | 4 +- .../unit/test_meshes/test_user_1D_submesh.py | 72 +++++++++++++++++++ .../test_lithium_ion/test_dfn.py | 18 +++++ .../test_lithium_ion/test_spm.py | 29 ++++++++ .../test_lithium_ion/test_spme.py | 25 +++++++ .../test_x_lumped_1D_set_temperature.py | 40 +++++++++++ 16 files changed, 243 insertions(+), 13 deletions(-) create mode 100644 docs/source/models/submodels/current_collector/set_potential_single_particle.rst rename results/2plus1D/{set-temperature-spm-1plus1D.py => set_temperature_spm_1plus1D.py} (100%) create mode 100644 tests/unit/test_meshes/test_user_1D_submesh.py create mode 100644 tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py diff --git a/docs/source/models/submodels/current_collector/index.rst b/docs/source/models/submodels/current_collector/index.rst index caf1d70df5..3ae2821bfd 100644 --- a/docs/source/models/submodels/current_collector/index.rst +++ b/docs/source/models/submodels/current_collector/index.rst @@ -11,5 +11,4 @@ Current Collector potential_pair quite_conductive_potential_pair single_particle_potential_pair - - + set_potential_single_particle diff --git a/docs/source/models/submodels/current_collector/set_potential_single_particle.rst b/docs/source/models/submodels/current_collector/set_potential_single_particle.rst new file mode 100644 index 0000000000..c96067f274 --- /dev/null +++ b/docs/source/models/submodels/current_collector/set_potential_single_particle.rst @@ -0,0 +1,5 @@ +Set Potential Single Particle Models +==================================== + +.. autoclass:: pybamm.current_collector.SetPotentialSingleParticle1plus1D + :members: diff --git a/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst b/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst index 743c16e2ed..ea31df59d9 100644 --- a/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst +++ b/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst @@ -1,5 +1,5 @@ -1D current collector -===================== +Set Temperature 1D current collector +==================================== .. autoclass:: pybamm.thermal.x_lumped.SetTemperature1D :members: diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/pybamm/meshes/one_dimensional_submeshes.py index c5eae37116..2dcda98e5b 100644 --- a/pybamm/meshes/one_dimensional_submeshes.py +++ b/pybamm/meshes/one_dimensional_submeshes.py @@ -30,10 +30,18 @@ def near(x, point, tol=3e-16): return abs(x - point) < tol for tab in ["negative", "positive"]: - if near(tabs[tab]["z_centre"], 0): + tab_location = tabs[tab]["z_centre"] + if near(tab_location, 0): self.tabs[tab + " tab"] = "left" - elif near(tabs[tab]["z_centre"], l_z): + elif near(tab_location, l_z): self.tabs[tab + " tab"] = "right" + else: + raise pybamm.GeometryError( + """{} tab located at {}, but must be at either 0 or {} + (in dimensionless coordinates).""".format( + tab, tab_location, l_z + ) + ) class Uniform1DSubMesh(SubMesh1D): diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index f30395e204..cf8e87cfb1 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -39,14 +39,19 @@ class BaseBatteryModel(pybamm.BaseModel): * "current collector" : str, optional Sets the current collector model to use. Can be "uniform" (default), "potential pair", "potential pair quite conductive", "single particle - potential pair", "jelly roll" or "set external potential". + potential pair", "jelly roll" or "set external potential". The submodel + "single particle potential pair" can only be used with lithium-ion + single particle models. The submodel "set external potential" can only + be used with the SPM. * "particle" : str, optional Sets the submodel to use to describe behaviour within the particle. Can be "Fickian diffusion" (default) or "fast diffusion". * "thermal" : str, optional Sets the thermal model to use. Can be "isothermal" (default), "x-full", "x-lumped", "xyz-lumped", "lumped" or "set external - temperature". Must be "isothermal" for lead-acid models. + temperature". Must be "isothermal" for lead-acid models. If the + option "set external temperature" is selected then "dimensionality" + must be 1. * "thermal current collector" : bool, optional Whether to include thermal effects in the current collector in one-dimensional models (default is False). Note that this option @@ -273,6 +278,25 @@ def options(self, extra_options): raise pybamm.OptionError( "thermal effects not implemented for lead-acid models" ) + if options[ + "current collector" + ] == "single particle potenetial pair" and not isinstance( + self, (pybamm.lithium_ion.SPM, pybamm.lithium_ion.SPMe) + ): + raise pybamm.OptionError( + "option {} only compatible with SPM or SPMe".format( + options["current collector"] + ) + ) + if options["current collector"] == "set external potential" and not isinstance( + self, pybamm.lithium_ion.SPM + ): + raise pybamm.OptionError( + "option {} only compatible with SPM".format( + options["current collector"] + ) + ) + self._options = options def set_standard_output_variables(self): diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py index 3d8ab6248c..798b51406c 100644 --- a/pybamm/models/submodels/current_collector/set_potential_single_particle.py +++ b/pybamm/models/submodels/current_collector/set_potential_single_particle.py @@ -108,6 +108,13 @@ def get_fundamental_variables(self): i_boundary_cc = pybamm.standard_variables.i_boundary_cc variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) + # Hack to get the leading-order current collector current density + # Note that this should be different from the actual (composite) current + # collector current density for 2+1D models, but not sure how to implement this + # using current structure of lithium-ion models + variables["Leading-order current collector current density"] = variables[ + "Current collector current density" + ] return variables diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/pybamm/models/submodels/electrode/base_electrode.py index 28a9ce2446..c763050d57 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -13,7 +13,9 @@ class BaseElectrode(pybamm.BaseSubModel): The parameters to use for this submodel domain : str Either 'Negative' or 'Positive' - + set_positive_potential : bool, optional + If True the battery model sets the positve potential based on the current. + If False, the potential is specified by the user. Default is True. **Extends:** :class:`pybamm.BaseSubModel` """ diff --git a/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/pybamm/models/submodels/electrode/ohm/composite_ohm.py index 80968af63e..82798d48ad 100644 --- a/pybamm/models/submodels/electrode/ohm/composite_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/composite_ohm.py @@ -18,7 +18,6 @@ class Composite(BaseModel): domain : str Either 'Negative electrode' or 'Positive electrode' - **Extends:** :class:`pybamm.BaseOhm` """ diff --git a/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 8a15e3aa77..654aa8399b 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -16,7 +16,9 @@ class LeadingOrder(BaseModel): The parameters to use for this submodel domain : str Either 'Negative' or 'Positive' - + set_positive_potential : bool, optional + If True the battery model sets the positve potential based on the current. + If False, the potential is specified by the user. Default is True. **Extends:** :class:`pybamm.electrode.ohm.BaseModel` """ diff --git a/results/2plus1D/set-temperature-spm-1plus1D.py b/results/2plus1D/set_temperature_spm_1plus1D.py similarity index 100% rename from results/2plus1D/set-temperature-spm-1plus1D.py rename to results/2plus1D/set_temperature_spm_1plus1D.py diff --git a/tests/unit/test_meshes/test_meshes.py b/tests/unit/test_meshes/test_meshes.py index 26366ef4b8..e258cdbb54 100644 --- a/tests/unit/test_meshes/test_meshes.py +++ b/tests/unit/test_meshes/test_meshes.py @@ -292,8 +292,8 @@ def test_multiple_meshes_macro(self): "Separator thickness [m]": 0.2, "Positive electrode thickness [m]": 0.3, "Electrode height [m]": 0.4, - "Negative tab centre z-coordinate [m]": 0.2, - "Positive tab centre z-coordinate [m]": 0.2, + "Negative tab centre z-coordinate [m]": 0.0, + "Positive tab centre z-coordinate [m]": 0.4, } ) diff --git a/tests/unit/test_meshes/test_user_1D_submesh.py b/tests/unit/test_meshes/test_user_1D_submesh.py new file mode 100644 index 0000000000..5916401cf3 --- /dev/null +++ b/tests/unit/test_meshes/test_user_1D_submesh.py @@ -0,0 +1,72 @@ +import pybamm +import unittest +import numpy as np + + +class TestUser1DSubMesh(unittest.TestCase): + def test_exceptions(self): + lims = [[0, 1], [0, 1]] + edges = np.array([0, 0.3, 1]) + mesh = pybamm.GetUserSupplied1DSubMesh(edges) + # test too many lims + with self.assertRaises(pybamm.GeometryError): + mesh(lims, None) + lims = [0, 1] + + # error if len(edges) != npts+1 + with self.assertRaises(pybamm.GeometryError): + mesh(lims, 5) + + # error if lims[0] not equal to edges[0] + lims = [0.1, 1] + with self.assertRaises(pybamm.GeometryError): + mesh(lims, len(edges) - 1) + + # error if lims[-1] not equal to edges[-1] + lims = [0, 0.9] + with self.assertRaises(pybamm.GeometryError): + mesh(lims, len(edges) - 1) + + def test_mesh_creation_no_parameters(self): + r = pybamm.SpatialVariable( + "r", domain=["negative particle"], coord_sys="spherical polar" + ) + + geometry = { + "negative particle": { + "primary": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} + } + } + + edges = np.array([0, 0.3, 1]) + submesh_types = { + "negative particle": pybamm.GetUserSupplied1DSubMesh( + edges + ) + } + var_pts = {r: len(edges) - 1} + mesh = pybamm.Mesh(geometry, submesh_types, var_pts) + + # create mesh + mesh = pybamm.Mesh(geometry, submesh_types, var_pts) + + # check boundary locations + self.assertEqual(mesh["negative particle"][0].edges[0], 0) + self.assertEqual(mesh["negative particle"][0].edges[-1], 1) + + # check number of edges and nodes + self.assertEqual(len(mesh["negative particle"][0].nodes), var_pts[r]) + self.assertEqual( + len(mesh["negative particle"][0].edges), + len(mesh["negative particle"][0].nodes) + 1, + ) + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 7d72a0b19a..3fb5480675 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -147,6 +147,24 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() + @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") + def test_x_lumped_thermal_set_temperature_1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "set external temperature", + } + model = pybamm.lithium_ion.DFN(options) + model.check_well_posedness() + + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "set external temperature", + } + with self.assertRaises(NotImplementedError): + model = pybamm.lithium_ion.DFN(options) + @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"thermal": "isothermal"} diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 84bdea31ef..fcf1c346c5 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -34,6 +34,13 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() + options = { + "current collector": "single particle potential pair", + "dimensionality": 1, + } + model = pybamm.lithium_ion.SPM(options) + model.check_well_posedness() + options = { "current collector": "single particle potential pair", "dimensionality": 2, @@ -41,6 +48,10 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() + options = {"current collector": "set external potential", "dimensionality": 1} + model = pybamm.lithium_ion.SPM(options) + model.check_well_posedness() + def test_x_full_thermal_model_no_current_collector(self): options = {"thermal": "x-full"} model = pybamm.lithium_ion.SPM(options) @@ -150,6 +161,24 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() + @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") + def test_x_lumped_thermal_set_temperature_1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "set external temperature", + } + model = pybamm.lithium_ion.SPM(options) + model.check_well_posedness() + + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "set external temperature", + } + with self.assertRaises(NotImplementedError): + model = pybamm.lithium_ion.SPM(options) + @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"thermal": "isothermal"} diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 0f2403db51..53f2fee5a7 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -34,6 +34,13 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() + options = { + "current collector": "single particle potential pair", + "dimensionality": 1, + } + model = pybamm.lithium_ion.SPMe(options) + model.check_well_posedness() + options = { "current collector": "single particle potential pair", "dimensionality": 2, @@ -154,6 +161,24 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() + @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") + def test_x_lumped_thermal_set_temperature_1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "set external temperature", + } + model = pybamm.lithium_ion.SPMe(options) + model.check_well_posedness() + + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "set external temperature", + } + with self.assertRaises(NotImplementedError): + model = pybamm.lithium_ion.SPMe(options) + @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"thermal": "isothermal"} diff --git a/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py b/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py new file mode 100644 index 0000000000..d3ece87c76 --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py @@ -0,0 +1,40 @@ +# +# Test x-lumped submodel with 1D current collectors in which the temperature is +# set externally +# + +import pybamm +import tests +import unittest + +from tests.unit.test_models.test_submodels.test_thermal.coupled_variables import ( + coupled_variables, +) + + +class TestSetTemperature1D(unittest.TestCase): + def test_public_functions(self): + param = pybamm.standard_parameters_lithium_ion + phi_s_cn = pybamm.PrimaryBroadcast(pybamm.Scalar(0), ["current collector"]) + phi_s_cp = pybamm.PrimaryBroadcast(pybamm.Scalar(3), ["current collector"]) + + coupled_variables.update( + { + "Negative current collector potential": phi_s_cn, + "Positive current collector potential": phi_s_cp, + } + ) + + submodel = pybamm.thermal.x_lumped.SetTemperature1D(param) + std_tests = tests.StandardSubModelTests(submodel, coupled_variables) + std_tests.test_all() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() From e47b69c1ca9d670930569a168a6371734d3367c9 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Fri, 27 Sep 2019 10:42:58 +0100 Subject: [PATCH 030/122] #548 tidy up set potential submodel --- pybamm/models/reaction_diffusion.py | 6 +- .../base_current_collector.py | 9 ++- .../composite_potential_pair.py | 2 +- .../current_collector/potential_pair.py | 2 +- .../quite_conductive_potential_pair.py | 3 +- .../set_potential_single_particle.py | 75 +------------------ .../submodels/electrode/base_electrode.py | 12 +-- 7 files changed, 20 insertions(+), 89 deletions(-) diff --git a/pybamm/models/reaction_diffusion.py b/pybamm/models/reaction_diffusion.py index 7dc849aead..7a4d1e5c49 100644 --- a/pybamm/models/reaction_diffusion.py +++ b/pybamm/models/reaction_diffusion.py @@ -45,7 +45,11 @@ def set_electrolyte_submodel(self): self.param, self.reactions ) self.variables.update( - {"Positive current collector potential": pybamm.Scalar(0)} + { + "Positive current collector potential": pybamm.PrimaryBroadcast( + pybamm.Scalar(0), "current collector" + ) + } ) def set_interfacial_submodel(self): diff --git a/pybamm/models/submodels/current_collector/base_current_collector.py b/pybamm/models/submodels/current_collector/base_current_collector.py index 4b48dba561..fb1af13345 100644 --- a/pybamm/models/submodels/current_collector/base_current_collector.py +++ b/pybamm/models/submodels/current_collector/base_current_collector.py @@ -74,12 +74,15 @@ def _get_standard_potential_variables(self, phi_s_cn, phi_s_cp): pot_scale = self.param.potential_scale U_ref = self.param.U_p_ref - self.param.U_n_ref - # add more to this + # Local potential difference + V_cc = phi_s_cp - phi_s_cn + variables = { "Positive current collector potential": phi_s_cp, "Positive current collector potential [V]": U_ref + phi_s_cp * pot_scale, - "Local potenital difference": phi_s_cp - phi_s_cn, - "Local potenital difference [V]": U_ref + (phi_s_cp - phi_s_cn) * pot_scale, + "Local current collector potential difference": V_cc, + "Local current collector potential difference [V]": U_ref + + V_cc * pot_scale, } variables.update(self._get_standard_negative_potential_variables(phi_s_cn)) diff --git a/pybamm/models/submodels/current_collector/composite_potential_pair.py b/pybamm/models/submodels/current_collector/composite_potential_pair.py index ddab082fc5..9f8e131dec 100644 --- a/pybamm/models/submodels/current_collector/composite_potential_pair.py +++ b/pybamm/models/submodels/current_collector/composite_potential_pair.py @@ -1,5 +1,5 @@ # -# Class for two-dimensional current collectors - composite models +# Class for one- and two-dimensional composite potential pair current collector models # import pybamm from .potential_pair import ( diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index 0830696885..1ce4d1bbe9 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -1,5 +1,5 @@ # -# Class for two-dimensional current collectors +# Class for one- and two-dimensional potential pair current collector models # import pybamm from .base_current_collector import BaseModel diff --git a/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py b/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py index 1309fcddf5..e71b1307f4 100644 --- a/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py +++ b/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py @@ -1,5 +1,6 @@ # -# Class for two-dimensional current collectors +# Class for one- and two-dimensional potential pair "quite conductive" +# current collector models # import pybamm from .potential_pair import ( diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py index 798b51406c..e16fc3dfb3 100644 --- a/pybamm/models/submodels/current_collector/set_potential_single_particle.py +++ b/pybamm/models/submodels/current_collector/set_potential_single_particle.py @@ -1,10 +1,12 @@ # -# Class for one-dimensional current collectors +# Class for one-dimensional current collectors in which the potential is held +# fixed and the current is determined from the I-V relationship used in the SPM(e) # import pybamm +from .base_current_collector import BaseModel -class SetPotentialSingleParticle1plus1D(pybamm.BaseSubModel): +class SetPotentialSingleParticle1plus1D(BaseModel): """A submodel 1D current collectors which *doesn't* update the potentials during solve. This class uses the current-voltage relationship from the SPM(e) (see [1]_) to calculate the current. @@ -27,75 +29,6 @@ class SetPotentialSingleParticle1plus1D(pybamm.BaseSubModel): def __init__(self, param): super().__init__(param) - def _get_standard_potential_variables(self, phi_s_cn, phi_s_cp): - """ - A private function to obtain the standard variables which - can be derived from the potentials in the current collector. - - Parameters - ---------- - phi_cc : :class:`pybamm.Symbol` - The potential in the current collector. - - Returns - ------- - variables : dict - The variables which can be derived from the potential in the - current collector. - """ - param = self.param - - # Local potential difference - V_cc = phi_s_cp - phi_s_cn - - phi_neg_tab = pybamm.BoundaryValue(phi_s_cn, "negative tab") - phi_pos_tab = pybamm.BoundaryValue(phi_s_cp, "positive tab") - - variables = { - "Negative current collector potential": phi_s_cn, - "Negative current collector potential [V]": phi_s_cn - * param.potential_scale, - "Negative tab potential": phi_neg_tab, - "Negative tab potential [V]": phi_neg_tab * param.potential_scale, - "Positive tab potential": phi_pos_tab, - "Positive tab potential [V]": param.U_p_ref - - param.U_n_ref - + phi_pos_tab * param.potential_scale, - "Positive current collector potential": phi_s_cp, - "Positive current collector potential [V]": param.U_p_ref - - param.U_n_ref - + phi_s_cp * param.potential_scale, - "Local current collector potential difference": V_cc, - "Local current collector potential difference [V]": param.U_p_ref - - param.U_n_ref - + V_cc * param.potential_scale, - } - - return variables - - def _get_standard_current_variables(self, i_cc, i_boundary_cc): - """ - A private function to obtain the standard variables which - can be derived from the current in the current collector. - Parameters - ---------- - i_cc : :class:`pybamm.Symbol` - The current in the current collector. - i_boundary_cc : :class:`pybamm.Symbol` - The current leaving the current collector and going into the cell - Returns - ------- - variables : dict - The variables which can be derived from the current in the current - collector. - """ - - # TO DO: implement grad in 2D to get i_cc - # just need this to get 1D models working for now - variables = {"Current collector current density": i_boundary_cc} - - return variables - def get_fundamental_variables(self): phi_s_cn = pybamm.standard_variables.phi_s_cn diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/pybamm/models/submodels/electrode/base_electrode.py index c763050d57..fc986a694d 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -124,8 +124,6 @@ def _get_standard_whole_cell_variables(self, variables): The variables in the whole model with the whole-cell current variables added. """ - pot_scale = self.param.potential_scale - U_ref = self.param.U_p_ref - self.param.U_n_ref i_s_n = variables["Negative electrode current density"] i_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") @@ -135,21 +133,13 @@ def _get_standard_whole_cell_variables(self, variables): if self.set_positive_potential: phi_s_p = variables["Positive electrode potential"] - phi_s_cn = variables["Negative current collector potential"] phi_s_cp = pybamm.boundary_value(phi_s_p, "right") - v_boundary_cc = phi_s_cp - phi_s_cn - variables = { "Electrode current density": i_s, "Positive current collector potential": phi_s_cp, - "Local current collector potential difference": v_boundary_cc, - "Local current collector potential difference [V]": U_ref - + v_boundary_cc * pot_scale, } else: - variables = { - "Electrode current density": i_s, - } + variables = {"Electrode current density": i_s} return variables From 06837b9f145b5eb5a6a4e6bbcb941f9243df55fb Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Thu, 3 Oct 2019 10:37:19 +0100 Subject: [PATCH 031/122] #548 remove jelly roll --- .../full_battery_models/base_battery_model.py | 11 +----- .../submodels/current_collector/__init__.py | 1 - .../current_collector/potential_pair.py | 36 ------------------- .../2plus1D/set_temperature_spm_1plus1D.py | 2 +- results/2plus1D/spm_1plus1D.py | 2 +- results/2plus1D/user_mesh_spm_1plus1D.py | 6 +++- ...esh.py => test_one_dimensional_submesh.py} | 28 ++++++++++++--- .../test_meshes/test_scikit_fem_submesh.py | 2 +- tests/unit/test_meshes/test_submesh.py | 0 .../unit/test_meshes/test_uniform_submesh.py | 19 ---------- .../test_potential_pair.py | 6 +--- 11 files changed, 33 insertions(+), 80 deletions(-) rename tests/unit/test_meshes/{test_user_1D_submesh.py => test_one_dimensional_submesh.py} (69%) delete mode 100644 tests/unit/test_meshes/test_submesh.py delete mode 100644 tests/unit/test_meshes/test_uniform_submesh.py diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index fec36ea5ea..18c99dd81a 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -39,7 +39,7 @@ class BaseBatteryModel(pybamm.BaseModel): * "current collector" : str, optional Sets the current collector model to use. Can be "uniform" (default), "potential pair", "potential pair quite conductive", "single particle - potential pair", "jelly roll" or "set external potential". The submodel + potential pair" or "set external potential". The submodel "single particle potential pair" can only be used with lithium-ion single particle models. The submodel "set external potential" can only be used with the SPM. @@ -231,7 +231,6 @@ def options(self, extra_options): "potential pair quite conductive", "single particle potential pair", "set external potential", - "jelly roll", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( @@ -611,14 +610,6 @@ def set_current_collector_submodel(self): """Set potential model only implemented for 1D current collectors""" ) - elif self.options["current collector"] == "jelly roll": - if self.options["dimensionality"] == 1: - submodel = pybamm.current_collector.PotentialPairUnrolled(self.param) - elif self.options["dimensionality"] in [0, 2]: - raise NotImplementedError( - """Jelly roll model only implemented for 1D current - collectors""" - ) self.submodels["current collector"] = submodel def set_voltage_variables(self): diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index 47b084cc65..a71756c6d6 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -7,7 +7,6 @@ BasePotentialPair, PotentialPair1plus1D, PotentialPair2plus1D, - PotentialPairUnrolled ) from .composite_potential_pair import ( BaseCompositePotentialPair, diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index 1ce4d1bbe9..abd365dd85 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -174,39 +174,3 @@ def set_boundary_conditions(self, variables): def _get_effective_current_collector_area(self): "Return the area of the current collector" return self.param.l_y * self.param.l_z - - -class PotentialPairUnrolled(PotentialPair1plus1D): - """ - Base class for 1+1D model used to model an 'unrolled' jelly roll with a tab - at either end - """ - - def set_boundary_conditions(self, variables): - - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = variables["Positive current collector potential"] - - param = self.param - applied_current = param.current_with_time - cc_area = self._get_effective_current_collector_area() - - # cc_area appears here due to choice of non-dimensionalisation - pos_tab_bc = ( - -applied_current - * cc_area - / (param.sigma_cp * param.delta ** 2 * param.l_cp) - ) - - # Boundary condition needs to be on the variables that go into the Laplacian, - # even though phi_s_cp isn't a pybamm.Variable object - self.boundary_conditions = { - phi_s_cn: { - "left": (pybamm.Scalar(0), "Dirichlet"), - "right": (pybamm.Scalar(0), "Neumann"), - }, - phi_s_cp: { - "left": (pybamm.Scalar(0), "Neumann"), - "right": (pos_tab_bc, "Neumann"), - }, - } diff --git a/results/2plus1D/set_temperature_spm_1plus1D.py b/results/2plus1D/set_temperature_spm_1plus1D.py index 4eafcdf583..f5ffc9cdcd 100644 --- a/results/2plus1D/set_temperature_spm_1plus1D.py +++ b/results/2plus1D/set_temperature_spm_1plus1D.py @@ -9,7 +9,7 @@ # load (1+1D) SPM model options = { - "current collector": "jelly roll", + "current collector": "potential pair", "dimensionality": 1, "thermal": "set external temperature", } diff --git a/results/2plus1D/spm_1plus1D.py b/results/2plus1D/spm_1plus1D.py index feb5c31166..8665d93767 100644 --- a/results/2plus1D/spm_1plus1D.py +++ b/results/2plus1D/spm_1plus1D.py @@ -7,7 +7,7 @@ # load (1+1D) SPMe model options = { - "current collector": "jelly roll", + "current collector": "potential pair", "dimensionality": 1, "thermal": "lumped", } diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py index 997a8ecaaa..d38bdd7e8c 100644 --- a/results/2plus1D/user_mesh_spm_1plus1D.py +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -6,7 +6,11 @@ pybamm.set_logging_level("INFO") # load (1+1D) SPMe model -options = {"current collector": "jelly roll", "dimensionality": 1, "thermal": "lumped"} +options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "lumped", +} model = pybamm.lithium_ion.SPM(options) # create geometry diff --git a/tests/unit/test_meshes/test_user_1D_submesh.py b/tests/unit/test_meshes/test_one_dimensional_submesh.py similarity index 69% rename from tests/unit/test_meshes/test_user_1D_submesh.py rename to tests/unit/test_meshes/test_one_dimensional_submesh.py index 5916401cf3..6b2a0399ea 100644 --- a/tests/unit/test_meshes/test_user_1D_submesh.py +++ b/tests/unit/test_meshes/test_one_dimensional_submesh.py @@ -3,6 +3,28 @@ import numpy as np +class TestSubMesh1D(unittest.TestCase): + def test_tabs(self): + edges = np.linspace(0, 1, 10) + tabs = {"negative": {"z_centre": 0}, "positive": {"z_centre": 1}} + mesh = pybamm.SubMesh1D(edges, None, tabs=tabs) + self.assertEqual(mesh.tabs["negative tab"], "left") + self.assertEqual(mesh.tabs["positive tab"], "right") + + def test_exceptions(self): + edges = np.linspace(0, 1, 10) + tabs = {"negative": {"z_centre": 0.2}, "positive": {"z_centre": 1}} + with self.assertRaises(pybamm.GeometryError): + pybamm.SubMesh1D(edges, None, tabs=tabs) + + +class TestUniform1DSubMesh(unittest.TestCase): + def test_exceptions(self): + lims = [[0, 1], [0, 1]] + with self.assertRaises(pybamm.GeometryError): + pybamm.Uniform1DSubMesh(lims, None) + + class TestUser1DSubMesh(unittest.TestCase): def test_exceptions(self): lims = [[0, 1], [0, 1]] @@ -39,11 +61,7 @@ def test_mesh_creation_no_parameters(self): } edges = np.array([0, 0.3, 1]) - submesh_types = { - "negative particle": pybamm.GetUserSupplied1DSubMesh( - edges - ) - } + submesh_types = {"negative particle": pybamm.GetUserSupplied1DSubMesh(edges)} var_pts = {r: len(edges) - 1} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) diff --git a/tests/unit/test_meshes/test_scikit_fem_submesh.py b/tests/unit/test_meshes/test_scikit_fem_submesh.py index 0858359db1..181c579e00 100644 --- a/tests/unit/test_meshes/test_scikit_fem_submesh.py +++ b/tests/unit/test_meshes/test_scikit_fem_submesh.py @@ -168,7 +168,7 @@ def test_tab_left_right(self): } ) - # check error raised if tab not on boundary + # check mesh can be built geometry = pybamm.Geometryxp1DMacro(cc_dimension=2) param.process_geometry(geometry) mesh_type(geometry, submesh_types, var_pts) diff --git a/tests/unit/test_meshes/test_submesh.py b/tests/unit/test_meshes/test_submesh.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/test_meshes/test_uniform_submesh.py b/tests/unit/test_meshes/test_uniform_submesh.py deleted file mode 100644 index 17b5339f6a..0000000000 --- a/tests/unit/test_meshes/test_uniform_submesh.py +++ /dev/null @@ -1,19 +0,0 @@ -import pybamm -import unittest - - -class TestUniform1DSubMesh(unittest.TestCase): - def test_exceptions(self): - lims = [[0, 1], [0, 1]] - with self.assertRaises(pybamm.GeometryError): - pybamm.Uniform1DSubMesh(lims, None) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py b/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py index 656182bdb5..f1f8d0dffc 100644 --- a/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py @@ -10,21 +10,17 @@ class TestBaseModel(unittest.TestCase): def test_public_functions(self): param = pybamm.standard_parameters_lithium_ion - submodel = pybamm.current_collector.PotentialPair1plus1D(param) variables = { "Positive current collector potential": pybamm.PrimaryBroadcast( 0, "current collector" ) } + submodel = pybamm.current_collector.PotentialPair1plus1D(param) std_tests = tests.StandardSubModelTests(submodel, variables) - std_tests.test_all() submodel = pybamm.current_collector.PotentialPair2plus1D(param) std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() - submodel = pybamm.current_collector.PotentialPairUnrolled(param) - std_tests = tests.StandardSubModelTests(submodel, variables) - std_tests.test_all() if __name__ == "__main__": From 802cfd0ca50a50b09436b8356c9c9250c09e5e95 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 15 Oct 2019 09:41:14 -0400 Subject: [PATCH 032/122] #664 convert scalars and operations to casadi --- .requirements-docs.txt | 1 + docs/source/expression_tree/index.rst | 3 +- .../operations/convert_to_casadi.rst | 5 ++ .../{ => operations}/evaluate.rst | 0 .../expression_tree/operations/index.rst | 10 +++ .../{ => operations}/simplify.rst | 0 docs/source/solvers/casadi_solver.rst | 5 ++ docs/source/solvers/index.rst | 1 + pybamm/__init__.py | 7 +- pybamm/expression_tree/operations/__init__.py | 0 .../operations/convert_to_casadi.py | 75 +++++++++++++++++++ .../{ => operations}/evaluate.py | 0 .../{ => operations}/simplify.py | 0 pybamm/expression_tree/symbol.py | 7 ++ pybamm/solvers/casadi_solver.py | 9 +++ setup.py | 1 + .../test_operations/__init__.py | 0 .../test_operations/test_convert_to_casadi.py | 62 +++++++++++++++ .../{ => test_operations}/test_copy.py | 0 .../{ => test_operations}/test_evaluate.py | 0 .../{ => test_operations}/test_jac.py | 0 .../{ => test_operations}/test_jac_2D.py | 0 .../{ => test_operations}/test_simplify.py | 0 23 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 docs/source/expression_tree/operations/convert_to_casadi.rst rename docs/source/expression_tree/{ => operations}/evaluate.rst (100%) create mode 100644 docs/source/expression_tree/operations/index.rst rename docs/source/expression_tree/{ => operations}/simplify.rst (100%) create mode 100644 docs/source/solvers/casadi_solver.rst create mode 100644 pybamm/expression_tree/operations/__init__.py create mode 100644 pybamm/expression_tree/operations/convert_to_casadi.py rename pybamm/expression_tree/{ => operations}/evaluate.py (100%) rename pybamm/expression_tree/{ => operations}/simplify.py (100%) create mode 100644 pybamm/solvers/casadi_solver.py create mode 100644 tests/unit/test_expression_tree/test_operations/__init__.py create mode 100644 tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py rename tests/unit/test_expression_tree/{ => test_operations}/test_copy.py (100%) rename tests/unit/test_expression_tree/{ => test_operations}/test_evaluate.py (100%) rename tests/unit/test_expression_tree/{ => test_operations}/test_jac.py (100%) rename tests/unit/test_expression_tree/{ => test_operations}/test_jac_2D.py (100%) rename tests/unit/test_expression_tree/{ => test_operations}/test_simplify.py (100%) diff --git a/.requirements-docs.txt b/.requirements-docs.txt index 1aa9e60102..c7bcd6446f 100644 --- a/.requirements-docs.txt +++ b/.requirements-docs.txt @@ -5,5 +5,6 @@ pandas>=0.23 anytree>=2.4.3 autograd>=1.2 scikit-fem>=0.2.0 +casadi>=3.5.0 guzzle-sphinx-theme sphinx>=1.5 diff --git a/docs/source/expression_tree/index.rst b/docs/source/expression_tree/index.rst index 2e95d07b16..3eedd7b622 100644 --- a/docs/source/expression_tree/index.rst +++ b/docs/source/expression_tree/index.rst @@ -15,7 +15,6 @@ Expression Tree unary_operator concatenations broadcasts - simplify functions interpolant - evaluate + operations/index diff --git a/docs/source/expression_tree/operations/convert_to_casadi.rst b/docs/source/expression_tree/operations/convert_to_casadi.rst new file mode 100644 index 0000000000..76a6ed614e --- /dev/null +++ b/docs/source/expression_tree/operations/convert_to_casadi.rst @@ -0,0 +1,5 @@ +Convert to CasADi +================= + +.. autoclass:: pybamm.CasadiConverter + :members: diff --git a/docs/source/expression_tree/evaluate.rst b/docs/source/expression_tree/operations/evaluate.rst similarity index 100% rename from docs/source/expression_tree/evaluate.rst rename to docs/source/expression_tree/operations/evaluate.rst diff --git a/docs/source/expression_tree/operations/index.rst b/docs/source/expression_tree/operations/index.rst new file mode 100644 index 0000000000..108c6eeedf --- /dev/null +++ b/docs/source/expression_tree/operations/index.rst @@ -0,0 +1,10 @@ +Operations on expression trees +============================== + +Classes and functions that operate on the expression tree + +.. toctree:: + + simplify + evaluate + convert_to_casadi diff --git a/docs/source/expression_tree/simplify.rst b/docs/source/expression_tree/operations/simplify.rst similarity index 100% rename from docs/source/expression_tree/simplify.rst rename to docs/source/expression_tree/operations/simplify.rst diff --git a/docs/source/solvers/casadi_solver.rst b/docs/source/solvers/casadi_solver.rst new file mode 100644 index 0000000000..f21e3c74cf --- /dev/null +++ b/docs/source/solvers/casadi_solver.rst @@ -0,0 +1,5 @@ +Casadi Solver +============= + +.. autoclass:: pybamm.CasadiSolver + :members: diff --git a/docs/source/solvers/index.rst b/docs/source/solvers/index.rst index 6cc6c0021d..d85865dec7 100644 --- a/docs/source/solvers/index.rst +++ b/docs/source/solvers/index.rst @@ -7,4 +7,5 @@ Solvers base_solvers scipy_solver scikits_solvers + casadi_solver solution diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 7723eb8742..dda95eea37 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -150,18 +150,21 @@ def version(formatted=False): UndefinedOperationError, GeometryError, ) -from .expression_tree.simplify import ( + +# Operations +from .expression_tree.operations.simplify import ( Simplification, simplify_if_constant, simplify_addition_subtraction, simplify_multiplication_division, ) -from .expression_tree.evaluate import ( +from .expression_tree.operations.evaluate import ( find_symbols, id_to_python_variable, to_python, EvaluatorPython, ) +from .expression_tree.operations.convert_to_casadi import CasadiConverter # # Model classes diff --git a/pybamm/expression_tree/operations/__init__.py b/pybamm/expression_tree/operations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py new file mode 100644 index 0000000000..41a7251c33 --- /dev/null +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -0,0 +1,75 @@ +# +# Convert a PyBaMM expression tree to a CasADi expression tree +# +import pybamm +import casadi + + +class CasadiConverter(object): + def __init__(self, casadi_symbols=None): + self._casadi_symbols = casadi_symbols or {} + + def convert(self, symbol): + """ + This function recurses down the tree, applying any simplifications defined in + classes derived from pybamm.Symbol. E.g. any expression multiplied by a + pybamm.Scalar(0) will be simplified to a pybamm.Scalar(0). + If a symbol has already been simplified, the stored value is returned. + + Parameters + ---------- + symbol : :class:`pybamm.Symbol` + The symbol to convert + + Returns + ------- + CasADi symbol + The convert symbol + """ + + try: + return self._casadi_symbols[symbol.id] + except KeyError: + casadi_symbol = self._convert(symbol) + self._casadi_symbols[symbol.id] = casadi_symbol + + return casadi_symbol + + def _convert(self, symbol): + """ See :meth:`Simplification.convert()`. """ + if isinstance(symbol, pybamm.Scalar): + return casadi.SX(symbol.evaluate()) + + if isinstance(symbol, pybamm.BinaryOperator): + left, right = symbol.children + # process children + converted_left = self.convert(left) + converted_right = self.convert(right) + # _binary_evaluate defined in derived classes for specific rules + return symbol._binary_evaluate(converted_left, converted_right) + + elif isinstance(symbol, pybamm.UnaryOperator): + converted_child = self.convert(symbol.child) + if isinstance(symbol, pybamm.AbsoluteValue): + return casadi.fabs(converted_child) + return symbol._unary_evaluate(converted_child) + + elif isinstance(symbol, pybamm.Function): + converted_children = [None] * len(symbol.children) + for i, child in enumerate(symbol.children): + converted_children[i] = self.convert(child) + return symbol._function_evaluate(converted_children) + + elif isinstance(symbol, pybamm.Concatenation): + converted_children = [self.convert(child) for child in symbol.children] + return symbol._concatenation_evaluate(converted_children) + + else: + raise TypeError( + """ + Cannot convert symbol of type '{}' to CasADi. Symbols must all be + 'linear algebra' at this stage. + """.format( + type(symbol) + ) + ) diff --git a/pybamm/expression_tree/evaluate.py b/pybamm/expression_tree/operations/evaluate.py similarity index 100% rename from pybamm/expression_tree/evaluate.py rename to pybamm/expression_tree/operations/evaluate.py diff --git a/pybamm/expression_tree/simplify.py b/pybamm/expression_tree/operations/simplify.py similarity index 100% rename from pybamm/expression_tree/simplify.py rename to pybamm/expression_tree/operations/simplify.py diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index 8bbbb8db99..633875c492 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -584,6 +584,13 @@ def simplify(self, simplified_symbols=None): """ Simplify the expression tree. See :class:`pybamm.Simplification`. """ return pybamm.Simplification(simplified_symbols).simplify(self) + def to_casadi(self, casadi_symbols=None): + """ + Convert the expression tree to a CasADi expression tree. + See :class:`pybamm.CasadiConverter`. + """ + return pybamm.CasadiConverter(casadi_symbols).convert(self) + def new_copy(self): """ Make a new copy of a symbol, to avoid Tree corruption errors while bypassing diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py new file mode 100644 index 0000000000..dc0e0acc9f --- /dev/null +++ b/pybamm/solvers/casadi_solver.py @@ -0,0 +1,9 @@ +# +# Wrap CasADi +# +import pybamm +import casadi + + +class CasadiSolver(pybamm.DaeSolver): + pass diff --git a/setup.py b/setup.py index 14227d4205..8e9dac78cd 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ def load_version(): "anytree>=2.4.3", "autograd>=1.2", "scikit-fem>=0.2.0", + "casadi>=3.5.0", # Note: Matplotlib is loaded for debug plots, but to ensure pybamm runs # on systems without an attached display, it should never be imported # outside of plot() methods. diff --git a/tests/unit/test_expression_tree/test_operations/__init__.py b/tests/unit/test_expression_tree/test_operations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py new file mode 100644 index 0000000000..7de38d55f1 --- /dev/null +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -0,0 +1,62 @@ +# +# Test for the Simplify class +# +import casadi +import math +import numpy as np +import pybamm +import unittest +from tests import get_discretisation_for_testing + + +class TestCasadiConverter(unittest.TestCase): + def test_convert_scalar_symbols(self): + a = pybamm.Scalar(0) + b = pybamm.Scalar(1) + c = pybamm.Scalar(-1) + d = pybamm.Scalar(2) + + self.assertEqual(a.to_casadi(), casadi.SX(0)) + self.assertEqual(d.to_casadi(), casadi.SX(2)) + + # negate + self.assertEqual((-b).to_casadi(), casadi.SX(-1)) + # absolute value + self.assertEqual(abs(c).to_casadi(), casadi.SX(1)) + + # function + def sin(x): + return np.sin(x) + + f = pybamm.Function(sin, b) + self.assertEqual((f).to_casadi(), casadi.SX(np.sin(1))) + + def myfunction(x, y): + return x + y + + f = pybamm.Function(myfunction, b, d) + self.assertEqual((f).to_casadi(), casadi.SX(3)) + + # addition + self.assertEqual((a + b).to_casadi(), casadi.SX(1)) + # subtraction + self.assertEqual((c - d).to_casadi(), casadi.SX(-3)) + # multiplication + self.assertEqual((c * d).to_casadi(), casadi.SX(-2)) + # power + self.assertEqual((c ** d).to_casadi(), casadi.SX(1)) + # division + self.assertEqual((b / d).to_casadi(), casadi.SX(1 / 2)) + + def test_convert_array_symbols(self): + pass + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_expression_tree/test_copy.py b/tests/unit/test_expression_tree/test_operations/test_copy.py similarity index 100% rename from tests/unit/test_expression_tree/test_copy.py rename to tests/unit/test_expression_tree/test_operations/test_copy.py diff --git a/tests/unit/test_expression_tree/test_evaluate.py b/tests/unit/test_expression_tree/test_operations/test_evaluate.py similarity index 100% rename from tests/unit/test_expression_tree/test_evaluate.py rename to tests/unit/test_expression_tree/test_operations/test_evaluate.py diff --git a/tests/unit/test_expression_tree/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py similarity index 100% rename from tests/unit/test_expression_tree/test_jac.py rename to tests/unit/test_expression_tree/test_operations/test_jac.py diff --git a/tests/unit/test_expression_tree/test_jac_2D.py b/tests/unit/test_expression_tree/test_operations/test_jac_2D.py similarity index 100% rename from tests/unit/test_expression_tree/test_jac_2D.py rename to tests/unit/test_expression_tree/test_operations/test_jac_2D.py diff --git a/tests/unit/test_expression_tree/test_simplify.py b/tests/unit/test_expression_tree/test_operations/test_simplify.py similarity index 100% rename from tests/unit/test_expression_tree/test_simplify.py rename to tests/unit/test_expression_tree/test_operations/test_simplify.py From e9b4b109d784ec61c7725fdcdc7e0b048583445e Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 15 Oct 2019 23:57:37 -0400 Subject: [PATCH 033/122] #664 convert more symbols to casadi --- examples/scripts/compare_lithium_ion.py | 6 +- pybamm/__init__.py | 10 +- .../operations/convert_to_casadi.py | 38 ++++-- pybamm/expression_tree/state_vector.py | 2 +- pybamm/expression_tree/symbol.py | 4 +- pybamm/geometry/geometry.py | 13 +- pybamm/models/standard_variables.py | 6 +- .../submodels/electrode/ohm/composite_ohm.py | 1 - .../base_electrolyte_conductivity.py | 3 +- .../submodels/particle/base_particle.py | 6 +- pybamm/parameters/electrical_parameters.py | 6 +- pybamm/parameters/parameter_sets.py | 1 - pybamm/processed_variable.py | 6 +- pybamm/solvers/casadi_solver.py | 127 +++++++++++++++++- .../test_operations/test_convert_to_casadi.py | 19 ++- 15 files changed, 198 insertions(+), 50 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 1aca4c7816..7f2d3a283d 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -19,8 +19,8 @@ options = {"thermal": "isothermal"} models = [ pybamm.lithium_ion.SPM(options), - pybamm.lithium_ion.SPMe(options), - pybamm.lithium_ion.DFN(options), + # pybamm.lithium_ion.SPMe(options), + # pybamm.lithium_ion.DFN(options), ] @@ -47,7 +47,7 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): - solutions[i] = model.default_solver.solve(model, t_eval) + solutions[i] = pybamm.CasadiSolver().solve(model, t_eval) # plot plot = pybamm.QuickPlot(models, mesh, solutions) diff --git a/pybamm/__init__.py b/pybamm/__init__.py index dda95eea37..e525fdd640 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -25,7 +25,7 @@ def _load_version_int(): __version_int__ = _load_version_int() __version__ = ".".join([str(x) for x in __version_int__]) if sys.version_info[0] < 3: - del (x) # Before Python3, list comprehension iterators leaked + del x # Before Python3, list comprehension iterators leaked # # Expose PyBaMM version @@ -248,11 +248,11 @@ def version(formatted=False): from .solvers.base_solver import BaseSolver from .solvers.ode_solver import OdeSolver from .solvers.dae_solver import DaeSolver -from .solvers.scipy_solver import ScipySolver -from .solvers.scikits_dae_solver import ScikitsDaeSolver -from .solvers.scikits_ode_solver import ScikitsOdeSolver -from .solvers.scikits_ode_solver import have_scikits_odes from .solvers.algebraic_solver import AlgebraicSolver +from .solvers.casadi_solver import CasadiSolver +from .solvers.scikits_dae_solver import ScikitsDaeSolver +from .solvers.scikits_ode_solver import ScikitsOdeSolver, have_scikits_odes +from .solvers.scipy_solver import ScipySolver # # Current profiles diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 41a7251c33..98b514d754 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -9,7 +9,7 @@ class CasadiConverter(object): def __init__(self, casadi_symbols=None): self._casadi_symbols = casadi_symbols or {} - def convert(self, symbol): + def convert(self, symbol, t=None, y=None): """ This function recurses down the tree, applying any simplifications defined in classes derived from pybamm.Symbol. E.g. any expression multiplied by a @@ -30,26 +30,34 @@ def convert(self, symbol): try: return self._casadi_symbols[symbol.id] except KeyError: - casadi_symbol = self._convert(symbol) + casadi_symbol = self._convert(symbol, t, y) self._casadi_symbols[symbol.id] = casadi_symbol return casadi_symbol - def _convert(self, symbol): - """ See :meth:`Simplification.convert()`. """ - if isinstance(symbol, pybamm.Scalar): - return casadi.SX(symbol.evaluate()) + def _convert(self, symbol, t, y): + """ See :meth:`CasadiConverter.convert()`. """ + if isinstance(symbol, (pybamm.Scalar, pybamm.Array, pybamm.Time)): + return casadi.SX(symbol.evaluate(t, y)) - if isinstance(symbol, pybamm.BinaryOperator): + elif isinstance(symbol, pybamm.StateVector): + if y is None: + raise ValueError("Must provide a 'y' for converting state vectors") + return casadi.vertcat(*[y[y_slice] for y_slice in symbol.y_slices]) + + elif isinstance(symbol, pybamm.BinaryOperator): left, right = symbol.children # process children - converted_left = self.convert(left) - converted_right = self.convert(right) - # _binary_evaluate defined in derived classes for specific rules - return symbol._binary_evaluate(converted_left, converted_right) + converted_left = self.convert(left, t, y) + converted_right = self.convert(right, t, y) + if isinstance(symbol, pybamm.Outer): + return casadi.outer_prod(converted_left, converted_right) + else: + # _binary_evaluate defined in derived classes for specific rules + return symbol._binary_evaluate(converted_left, converted_right) elif isinstance(symbol, pybamm.UnaryOperator): - converted_child = self.convert(symbol.child) + converted_child = self.convert(symbol.child, t, y) if isinstance(symbol, pybamm.AbsoluteValue): return casadi.fabs(converted_child) return symbol._unary_evaluate(converted_child) @@ -57,11 +65,13 @@ def _convert(self, symbol): elif isinstance(symbol, pybamm.Function): converted_children = [None] * len(symbol.children) for i, child in enumerate(symbol.children): - converted_children[i] = self.convert(child) + converted_children[i] = self.convert(child, t, y) return symbol._function_evaluate(converted_children) elif isinstance(symbol, pybamm.Concatenation): - converted_children = [self.convert(child) for child in symbol.children] + converted_children = [ + self.convert(child, t, y) for child in symbol.children + ] return symbol._concatenation_evaluate(converted_children) else: diff --git a/pybamm/expression_tree/state_vector.py b/pybamm/expression_tree/state_vector.py index dbdd7c4251..c8f91b686c 100644 --- a/pybamm/expression_tree/state_vector.py +++ b/pybamm/expression_tree/state_vector.py @@ -113,7 +113,7 @@ def _base_evaluate(self, t=None, y=None): ) else: out = (y[: len(self._evaluation_array)])[self._evaluation_array] - if out.ndim == 1: + if isinstance(out, np.ndarray) and out.ndim == 1: out = out[:, np.newaxis] return out diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index 633875c492..1351be141f 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -584,12 +584,12 @@ def simplify(self, simplified_symbols=None): """ Simplify the expression tree. See :class:`pybamm.Simplification`. """ return pybamm.Simplification(simplified_symbols).simplify(self) - def to_casadi(self, casadi_symbols=None): + def to_casadi(self, t=None, y=None, casadi_symbols=None): """ Convert the expression tree to a CasADi expression tree. See :class:`pybamm.CasadiConverter`. """ - return pybamm.CasadiConverter(casadi_symbols).convert(self) + return pybamm.CasadiConverter(casadi_symbols).convert(self, t, y) def new_copy(self): """ diff --git a/pybamm/geometry/geometry.py b/pybamm/geometry/geometry.py index bb7389b8c3..4d38bd3bb9 100644 --- a/pybamm/geometry/geometry.py +++ b/pybamm/geometry/geometry.py @@ -113,8 +113,8 @@ def add_domain(self, name, geometry): for k, v in geometry.items(): if k not in ["primary", "secondary", "tabs"]: raise ValueError( - "keys of geometry must be either \"primary\", \"secondary\" or " - "\"tabs\"" + 'keys of geometry must be either "primary", "secondary" or ' + '"tabs"' ) if k != "tabs": for variable, rnge in v.items(): @@ -135,15 +135,12 @@ def add_domain(self, name, geometry): else: for region, params in v.items(): if region not in ["negative", "positive"]: - raise ValueError( - "tabs region must be \"negative\" or \"positive\"" - - ) + raise ValueError('tabs region must be "negative" or "positive"') for pname in params.keys(): if pname not in ["y_centre", "z_centre", "width"]: raise ValueError( - "tabs region params must be \"y_centre\", " - "\"z_centre\" or \"width\"" + 'tabs region params must be "y_centre", ' + '"z_centre" or "width"' ) self.update({name: geometry}) diff --git a/pybamm/models/standard_variables.py b/pybamm/models/standard_variables.py index 277aeebe73..64636416c5 100644 --- a/pybamm/models/standard_variables.py +++ b/pybamm/models/standard_variables.py @@ -135,12 +135,10 @@ auxiliary_domains={"secondary": "current collector"}, ) c_s_n_surf_xav = pybamm.Variable( - "X-averaged negative particle surface concentration", - "current collector", + "X-averaged negative particle surface concentration", "current collector" ) c_s_p_surf_xav = pybamm.Variable( - "X-averaged positive particle surface concentration", - "current collector", + "X-averaged positive particle surface concentration", "current collector" ) diff --git a/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/pybamm/models/submodels/electrode/ohm/composite_ohm.py index fa6950beb3..8cd5efd504 100644 --- a/pybamm/models/submodels/electrode/ohm/composite_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/composite_ohm.py @@ -96,4 +96,3 @@ def set_boundary_conditions(self, variables): rbc = (-i_boundary_cc_0 / sigma_eff_0, "Neumann") self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - diff --git a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py index 47c6f69890..7d685dc7fe 100644 --- a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py +++ b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py @@ -61,8 +61,7 @@ def _get_standard_potential_variables(self, phi_e, phi_e_av): "Positive electrolyte potential": phi_e_p, "Positive electrolyte potential [V]": -param.U_n_ref + pot_scale * phi_e_p, "Electrolyte potential": phi_e, - "Electrolyte potential [V]": -param.U_n_ref - + pot_scale * phi_e, + "Electrolyte potential [V]": -param.U_n_ref + pot_scale * phi_e, "X-averaged electrolyte potential": phi_e_av, "X-averaged electrolyte potential [V]": -param.U_n_ref + pot_scale * phi_e_av, diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index ab3e5c7f0f..c8bf288fc1 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -53,11 +53,11 @@ def _get_standard_concentration_variables(self, c_s, c_s_xav): + self.domain.lower() + " particle surface concentration [mol.m-3]": c_scale * c_s_surf_av, self.domain + " electrode active volume fraction": active_volume, + self.domain + " electrode volume-averaged concentration": c_s_r_av_vol, self.domain - + " electrode volume-averaged concentration": c_s_r_av_vol, - self.domain + " electrode " + + " electrode " + "volume-averaged concentration [mol.m-3]": c_s_r_av_vol * c_scale, - self.domain + " electrode average extent of lithiation": c_s_r_av + self.domain + " electrode average extent of lithiation": c_s_r_av, } return variables diff --git a/pybamm/parameters/electrical_parameters.py b/pybamm/parameters/electrical_parameters.py index 36c4f6b215..f9260935c0 100644 --- a/pybamm/parameters/electrical_parameters.py +++ b/pybamm/parameters/electrical_parameters.py @@ -1,6 +1,7 @@ # # Standard electrical parameters # +import casadi import pybamm import numpy as np @@ -9,7 +10,10 @@ def abs_non_zero(x): if x == 0: # pragma: no cover return 1 else: - return abs(x) + if isinstance(x, casadi.SX): + return casadi.fabs(x) + else: + return abs(x) # -------------------------------------------------------------------------------------- diff --git a/pybamm/parameters/parameter_sets.py b/pybamm/parameters/parameter_sets.py index 943591434d..31c09106cc 100644 --- a/pybamm/parameters/parameter_sets.py +++ b/pybamm/parameters/parameter_sets.py @@ -46,4 +46,3 @@ "electrolyte": "sulfuric_acid_Sulzer2019", "experiment": "1C_discharge_from_full", } - diff --git a/pybamm/processed_variable.py b/pybamm/processed_variable.py index c7842ea10d..d2c78339c7 100644 --- a/pybamm/processed_variable.py +++ b/pybamm/processed_variable.py @@ -94,8 +94,10 @@ def __init__( self.base_eval = base_variable.evaluate(t_sol[0], u_sol[:, 0]) # handle 2D (in space) finite element variables differently - if mesh and "current collector" in self.domain and isinstance( - self.mesh[self.domain[0]][0], pybamm.Scikit2DSubMesh + if ( + mesh + and "current collector" in self.domain + and isinstance(self.mesh[self.domain[0]][0], pybamm.Scikit2DSubMesh) ): if len(self.t_sol) == 1: # space only (steady solution) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index dc0e0acc9f..228e6195d0 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -5,5 +5,128 @@ import casadi -class CasadiSolver(pybamm.DaeSolver): - pass +class CasadiSolver(pybamm.BaseSolver): + """Solve a discretised model, using CasADi. + + Parameters + ---------- + rtol : float, optional + The relative tolerance for the solver (default is 1e-6). + atol : float, optional + The absolute tolerance for the solver (default is 1e-6). + """ + + def __init__(self, method=None, rtol=1e-6, atol=1e-6): + super().__init__(method, rtol, atol) + + def compute_solution(self, model, t_eval): + """Calculate the solution of the model at specified times. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + t_eval : numeric type + The times at which to compute the solution + + """ + timer = pybamm.Timer() + + solve_start_time = timer.time() + pybamm.logger.info("Calling ODE solver") + solution = self.integrate( + self.dydt, + self.y0, + t_eval, + events=self.event_funs, + mass_matrix=model.mass_matrix.entries, + jacobian=self.jacobian, + ) + solve_time = timer.time() - solve_start_time + + # Identify the event that caused termination + termination = self.get_termination_reason(solution, self.events) + + return solution, solve_time, termination + + def set_up(self, model): + """Unpack model, perform checks, simplify and calculate jacobian. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + + Raises + ------ + :class:`pybamm.SolverError` + If the model contains any algebraic equations (in which case a DAE solver + should be used instead) + + """ + y0 = model.concatenated_initial_conditions[:, 0] + + t = casadi.SX.sym("t") + y_diff = casadi.SX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) + # y_alg = casadi.SX.sym("y_alg") + # create simplified rhs and event expressions + concatenated_rhs = model.concatenated_rhs.to_casadi(t, y_diff) + events = model.events.to_casadi(t, y_diff) + + # Create function to evaluate rhs + def dydt(t, y): + pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) + y = y[:, np.newaxis] + dy = concatenated_rhs.evaluate(t, y, known_evals={})[0] + return dy[:, 0] + + # Create event-dependent function to evaluate events + def event_fun(event): + def eval_event(t, y): + return event.evaluate(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in events.values()] + + # Create function to evaluate jacobian + if jac_rhs is not None: + + def jacobian(t, y): + return jac_rhs.evaluate(t, y, known_evals={})[0] + + else: + jacobian = None + + # Add the solver attributes + self.y0 = y0 + self.dydt = dydt + self.events = events + self.event_funs = event_funs + self.jacobian = jacobian + + def integrate( + self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None + ): + """ + Solve a model defined by dydt with initial conditions y0. + + Parameters + ---------- + derivs : method + A function that takes in t and y and returns the time-derivative dydt + y0 : numeric type + The initial conditions + t_eval : numeric type + The times at which to compute the solution + events : method, optional + A function that takes in t and y and returns conditions for the solver to + stop + mass_matrix : array_like, optional + The (sparse) mass matrix for the chosen spatial method. + jacobian : method, optional + A function that takes in t and y and returns the Jacobian + """ + raise NotImplementedError diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 7de38d55f1..11a0b80952 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -49,7 +49,24 @@ def myfunction(x, y): self.assertEqual((b / d).to_casadi(), casadi.SX(1 / 2)) def test_convert_array_symbols(self): - pass + # Arrays + a = np.array([1, 2, 3, 4, 5]) + pybamm_a = pybamm.Array(a) + self.assertTrue(casadi.is_equal(pybamm_a.to_casadi(), casadi.SX(a))) + + casadi_t = casadi.SX.sym("t") + casadi_y = casadi.SX.sym("y", 10) + + pybamm_t = pybamm.Time() + pybamm_y = pybamm.StateVector(slice(0, 10)) + + # Time + self.assertEqual(pybamm_t.to_casadi(casadi_t, casadi_y), casadi_t) + + # State Vector + self.assertTrue( + casadi.is_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) + ) if __name__ == "__main__": From 69a2f0b36745c5a8de4c4fa1b2cc84882f3fa114 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 16 Oct 2019 12:18:07 +0100 Subject: [PATCH 034/122] #548 fix results scripts --- CHANGELOG.md | 2 + results/2plus1D/set_potential_spm_1plus1D.py | 84 ------------------- .../2plus1D/set_temperature_spm_1plus1D.py | 61 ++++---------- results/2plus1D/user_mesh_spm_1plus1D.py | 4 +- 4 files changed, 22 insertions(+), 129 deletions(-) delete mode 100644 results/2plus1D/set_potential_spm_1plus1D.py diff --git a/CHANGELOG.md b/CHANGELOG.md index eb312ad6ee..1875219e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Allow parameters to be set by material or by specifying a particular paper (#647) - Set relative and absolute tolerances independently in solvers (#645) - Add some non-uniform meshes in 1D and 2D (#617) +- Adds submodels which allow the user to pass an array to set the entries of the `StateVector` + object corresponding to the temperature and potential in the 1+1D model formulation (#548) ## Optimizations diff --git a/results/2plus1D/set_potential_spm_1plus1D.py b/results/2plus1D/set_potential_spm_1plus1D.py deleted file mode 100644 index b9d8ecff4a..0000000000 --- a/results/2plus1D/set_potential_spm_1plus1D.py +++ /dev/null @@ -1,84 +0,0 @@ -# -# Example of 1+1D SPM where the potenital can be set by the user -# - -import pybamm -import numpy as np -import sys -import matplotlib.pyplot as plt - -plt.close("all") -# set logging level -pybamm.set_logging_level("INFO") - -# load (1+1D) SPM model -options = {"current collector": "set external potential", "dimensionality": 1} -model = pybamm.lithium_ion.SPM(options) -model.check_well_posedness() - -# create geometry -geometry = model.default_geometry - -# load parameter values and process model and geometry -param = model.default_parameter_values -param.process_model(model) -param.process_geometry(geometry) - -# set mesh -var = pybamm.standard_spatial_vars -var_pts = { - var.x_n: 10, - var.x_s: 5, - var.x_p: 10, - var.r_n: 10, - var.r_p: 10, - var.y: 1, - var.z: 5, -} -# depending on number of points in y-z plane may need to increase recursion depth... -sys.setrecursionlimit(10000) -mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) - -# discretise model -disc = pybamm.Discretisation(mesh, model.default_spatial_methods) -disc.process_model(model) - -# solve model -- simulate one hour discharge -tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) -t_end = 3600 / tau.evaluate(0) -t_eval = np.linspace(0, t_end, 10) -solution = model.default_solver.solve(model, t_eval) - -# e_conc = pybamm.ProcessedVariable( -# model.variables['Electrolyte concentration [mol.m-3]'], -# solution.t, -# solution.y, -# mesh=mesh, -# ) - -# plot -# plot = pybamm.QuickPlot(model, mesh, solution) -# plot.dynamic_plot() - - -def plot_var(var, time=-1): - variable = model.variables[var] - len_x = len(mesh.combine_submeshes(*variable.domain)) - len_z = variable.shape[0] // len_x - entries = np.empty((len_x, len_z, len(solution.t))) - - for idx in range(len(solution.t)): - t = solution.t[idx] - y = solution.y[:, idx] - entries[:, :, idx] = np.reshape(variable.evaluate(t, y), [len_x, len_z]) - plt.figure() - for bat_id in range(len_x): - plt.plot(range(len_z), entries[bat_id, :, time].flatten()) - plt.figure() - plt.imshow(entries[:, :, time]) - - -# plot_var(var="Electrolyte concentration") -plot_var(var="Interfacial current density", time=-1) -# plot_var(var="Current collector current density", time=[0]) -# plot_var(var="Local current collector potential difference", time=[0]) diff --git a/results/2plus1D/set_temperature_spm_1plus1D.py b/results/2plus1D/set_temperature_spm_1plus1D.py index f5ffc9cdcd..e10d3a1b93 100644 --- a/results/2plus1D/set_temperature_spm_1plus1D.py +++ b/results/2plus1D/set_temperature_spm_1plus1D.py @@ -1,9 +1,12 @@ +# +# Example of 1+1D SPM where the temperature can be set by the user +# + import pybamm import numpy as np import matplotlib.pyplot as plt import sys -plt.close("all") # set logging level pybamm.set_logging_level("INFO") @@ -24,7 +27,7 @@ param.process_geometry(geometry) # set mesh -nbat = 2 +nbat = 5 var = pybamm.standard_spatial_vars var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5, var.z: nbat} # depending on number of points in y-z plane may need to increase recursion depth... @@ -152,13 +155,15 @@ def non_dim_temperature(temperature): mesh=mesh, ) -# plot +# plots t_sec = param.process_symbol( pybamm.standard_parameters_lithium_ion.tau_discharge ).evaluate() t_hour = t_sec / (3600) -plt.figure() z = np.linspace(0, 1, nbat) + +# local voltage +plt.figure() for bat_id in range(nbat): plt.plot( solution1.t * t_hour, @@ -170,20 +175,16 @@ def non_dim_temperature(temperature): ) plt.xlabel("t [hrs]") plt.ylabel("Local voltage [V]") -plt.figure() -plt.plot( - solution1.t, voltage_step1(solution1.t), solution2.t, voltage_step2(solution2.t) -) -plt.xlabel("t") -plt.ylabel("Voltage [V]") -plt.show() + +# applied current plt.figure() plt.plot( solution1.t, current_step1(solution1.t), solution2.t, current_step2(solution2.t) ) plt.xlabel("t") plt.ylabel("Current [A]") -plt.show() + +# local heating plt.figure() z = np.linspace(0, 1, nbat) for bat_id in range(nbat): @@ -196,7 +197,8 @@ def non_dim_temperature(temperature): plt.xlabel("t [hrs]") plt.ylabel("X-averaged total heating [A.V.m-3]") plt.yscale("log") -plt.show() + +# local concentration plt.figure() for bat_id in range(nbat): plt.plot( @@ -207,7 +209,8 @@ def non_dim_temperature(temperature): ) plt.xlabel("t [hrs]") plt.ylabel("X-averaged positive particle surface concentration [mol.m-3]") -plt.show() + +# local temperature plt.figure() for bat_id in range(nbat): plt.plot( @@ -218,33 +221,5 @@ def non_dim_temperature(temperature): ) plt.xlabel("t [hrs]") plt.ylabel("X-averaged cell temperature [K]") -plt.show() - -def plot_var(var, solution, time=-1): - variable = model.variables[var] - len_x = len(mesh.combine_submeshes(*variable.domain)) - len_z = variable.shape[0] // len_x - entries = np.empty((len_x, len_z, len(solution.t))) - - for idx in range(len(solution.t)): - t = solution.t[idx] - y = solution.y[:, idx] - entries[:, :, idx] = np.reshape(variable.evaluate(t, y), [len_x, len_z]) - plt.figure() - for bat_id in range(len_x): - plt.plot(range(len_z), entries[bat_id, :, time].flatten()) - plt.title(var) - plt.figure() - plt.imshow(entries[:, :, time]) - plt.title(var) - - -# plot_var(var="Positive current collector potential", solution=solution1, time=-1) -# plot_var(var="Total heating [A.V.m-3]", solution=solution1, time=-1) -# plot_var(var="Interfacial current density", solution=solution2, time=-1) -# plot_var(var="Negative particle concentration [mol.m-3]", solution=solution2, time=-1) -# plot_var(var="Positive particle concentration [mol.m-3]", solution=solution2, time=-1) - -var_names = list(model.variables.keys()) -var_names.sort() +plt.show() diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py index 21a316f64c..42b9bcee31 100644 --- a/results/2plus1D/user_mesh_spm_1plus1D.py +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -5,7 +5,7 @@ # set logging level pybamm.set_logging_level("INFO") -# load (1+1D) SPMe model +# load (1+1D) SPM model options = { "current collector": "potential pair", "dimensionality": 1, @@ -35,7 +35,7 @@ param.process_geometry(geometry) # set mesh using user-supplied edges in z -z_edges = np.array([0, 0.03, 0.1, 0.3, 0.47, 0.5, 0.73, 0.8, 0.911, 1]) +z_edges = np.array([0, 0.025, 0.05, 0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 0.975, 1]) submesh_types = model.default_submesh_types submesh_types["current collector"] = pybamm.MeshGenerator( pybamm.UserSupplied1DSubMesh, submesh_params={"edges": z_edges} From 69aac748a9ed2fd7209aaccb64e89c0f2d74c220 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 17 Oct 2019 22:16:18 -0400 Subject: [PATCH 035/122] #664 add some more functions --- .flake8 | 3 +- examples/scripts/compare_lithium_ion.py | 4 +- .../operations/convert_to_casadi.py | 38 ++++++++++++++--- pybamm/solvers/casadi_solver.py | 10 +++-- .../test_operations/test_convert_to_casadi.py | 41 ++++++++++++++++++- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/.flake8 b/.flake8 index 9acc35b757..f7b9fdd18b 100644 --- a/.flake8 +++ b/.flake8 @@ -10,7 +10,8 @@ exclude= lib, lib64, share, - pyvenv.cfg + pyvenv.cfg, + third-party ignore= # False positive for white space before ':' on list slice # black should format these correctly diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 7f2d3a283d..41ad311860 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -18,9 +18,9 @@ # load models options = {"thermal": "isothermal"} models = [ - pybamm.lithium_ion.SPM(options), + # pybamm.lithium_ion.SPM(options), # pybamm.lithium_ion.SPMe(options), - # pybamm.lithium_ion.DFN(options), + pybamm.lithium_ion.DFN(options) ] diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 98b514d754..0f1a0d02bf 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -3,6 +3,7 @@ # import pybamm import casadi +import numpy as np class CasadiConverter(object): @@ -51,7 +52,7 @@ def _convert(self, symbol, t, y): converted_left = self.convert(left, t, y) converted_right = self.convert(right, t, y) if isinstance(symbol, pybamm.Outer): - return casadi.outer_prod(converted_left, converted_right) + return casadi.kron(converted_left, converted_right) else: # _binary_evaluate defined in derived classes for specific rules return symbol._binary_evaluate(converted_left, converted_right) @@ -63,16 +64,41 @@ def _convert(self, symbol, t, y): return symbol._unary_evaluate(converted_child) elif isinstance(symbol, pybamm.Function): - converted_children = [None] * len(symbol.children) - for i, child in enumerate(symbol.children): - converted_children[i] = self.convert(child, t, y) - return symbol._function_evaluate(converted_children) + converted_children = [ + self.convert(child, t, y) for child in symbol.children + ] + if symbol.function == np.min: + return casadi.mmin(*converted_children) + elif symbol.function == np.max: + return casadi.mmax(*converted_children) + else: + return symbol._function_evaluate(converted_children) elif isinstance(symbol, pybamm.Concatenation): converted_children = [ self.convert(child, t, y) for child in symbol.children ] - return symbol._concatenation_evaluate(converted_children) + if isinstance(symbol, (pybamm.NumpyConcatenation, pybamm.SparseStack)): + return casadi.vertcat(*converted_children) + # DomainConcatenation specifies a particular ordering for the concatenation, + # which we must follow + elif isinstance(symbol, pybamm.DomainConcatenation): + slice_starts = [] + all_child_vectors = [] + for i in range(symbol.secondary_dimensions_npts): + child_vectors = [] + for child_var, slices in zip( + converted_children, symbol._children_slices + ): + for child_dom, child_slice in slices.items(): + slice_starts.append(symbol._slices[child_dom][i].start) + child_vectors.append( + child_var[child_slice[i].start : child_slice[i].stop] + ) + all_child_vectors.extend( + [v for _, v in sorted(zip(slice_starts, child_vectors))] + ) + return casadi.vertcat(*all_child_vectors) else: raise TypeError( diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 228e6195d0..d8a6b02c92 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -70,10 +70,14 @@ def set_up(self, model): t = casadi.SX.sym("t") y_diff = casadi.SX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) - # y_alg = casadi.SX.sym("y_alg") + y_alg = casadi.SX.sym( + "y_alg", len(model.concatenated_algebraic.evaluate(0, y0)) + ) + y = casadi.vertcat(y_diff, y_alg) # create simplified rhs and event expressions - concatenated_rhs = model.concatenated_rhs.to_casadi(t, y_diff) - events = model.events.to_casadi(t, y_diff) + concatenated_rhs = model.concatenated_rhs.to_casadi(t, y) + concatenated_algebraic = model.concatenated_algebraic.to_casadi(t, y) + events = {name: event.to_casadi(t, y) for name, event in model.events.items()} # Create function to evaluate rhs def dydt(t, y): diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 11a0b80952..0eb7e34762 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -6,7 +6,7 @@ import numpy as np import pybamm import unittest -from tests import get_discretisation_for_testing +from tests import get_mesh_for_testing, get_1p1d_discretisation_for_testing class TestCasadiConverter(unittest.TestCase): @@ -68,6 +68,45 @@ def test_convert_array_symbols(self): casadi.is_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) ) + # outer product + outer = pybamm.Outer(pybamm_a, pybamm_a) + self.assertTrue(casadi.is_equal(outer.to_casadi(), casadi.SX(outer.evaluate()))) + + def test_special_functions(self): + a = np.array([1, 2, 3, 4, 5]) + pybamm_a = pybamm.Array(a) + self.assertEqual(pybamm.min(pybamm_a).to_casadi(), casadi.SX(1)) + + def test_concatenations(self): + y = np.linspace(0, 1, 10)[:, np.newaxis] + a = pybamm.Vector(y) + b = pybamm.Scalar(16) + c = pybamm.Scalar(3) + conc = pybamm.NumpyConcatenation(a, b, c) + self.assertTrue(casadi.is_equal(conc.to_casadi(), casadi.SX(conc.evaluate()))) + + # Domain concatenation + mesh = get_mesh_for_testing() + a_dom = ["negative electrode"] + b_dom = ["positive electrode"] + a = 2 * pybamm.Vector(np.ones_like(mesh[a_dom[0]][0].nodes), domain=a_dom) + b = pybamm.Vector(np.ones_like(mesh[b_dom[0]][0].nodes), domain=b_dom) + conc = pybamm.DomainConcatenation([b, a], mesh) + self.assertTrue(casadi.is_equal(conc.to_casadi(), casadi.SX(conc.evaluate()))) + + # 2d + disc = get_1p1d_discretisation_for_testing() + a = pybamm.Variable("a", domain=a_dom) + b = pybamm.Variable("b", domain=b_dom) + conc = pybamm.Concatenation(a, b) + disc.set_variable_slices([conc]) + expr = disc.process_symbol(conc) + y = casadi.SX.sym("y", expr.size) + x = expr.to_casadi(None, y) + f = casadi.Function("f", [x], [x]) + y_eval = np.linspace(0, 1, expr.size) + self.assertTrue(casadi.is_equal(f(y_eval), casadi.SX(expr.evaluate(y=y_eval)))) + if __name__ == "__main__": print("Add -v for more debug output") From 9572e8e9ff646bad4de53391d7745c8745dd694e Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Fri, 18 Oct 2019 21:08:59 -0400 Subject: [PATCH 036/122] #664 add casadi set up to solver --- examples/scripts/compare_lithium_ion.py | 6 +- pybamm/discretisations/discretisation.py | 2 +- pybamm/models/base_model.py | 18 +++-- pybamm/solvers/algebraic_solver.py | 4 +- pybamm/solvers/base_solver.py | 26 ++++-- pybamm/solvers/dae_solver.py | 4 +- pybamm/solvers/ode_solver.py | 81 ++++++++++++++++++- .../test_models/standard_model_tests.py | 3 +- 8 files changed, 120 insertions(+), 24 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 41ad311860..7fcfae8788 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -18,9 +18,9 @@ # load models options = {"thermal": "isothermal"} models = [ - # pybamm.lithium_ion.SPM(options), - # pybamm.lithium_ion.SPMe(options), - pybamm.lithium_ion.DFN(options) + pybamm.lithium_ion.SPM(options), + pybamm.lithium_ion.SPMe(options), + pybamm.lithium_ion.DFN(options), ] diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index a00e8f6aec..b96f04f7ba 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -127,7 +127,7 @@ def process_model(self, model, inplace=True): model_disc.options = model.options model_disc.use_jacobian = model.use_jacobian model_disc.use_simplify = model.use_simplify - model_disc.use_to_python = model.use_to_python + model_disc.convert_to_format = model.convert_to_format model_disc.bcs = self.bcs diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index 0470d86773..c2e4ab60bc 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -59,11 +59,17 @@ class BaseModel(object): Whether to simplify the expression tress representing the rhs and algebraic equations, Jacobain (if using) and events, before solving the model (default is True) - use_to_python : bool - Whether to convert the expression tress representing the rhs and - algebraic equations, Jacobain (if using) and events into pure python code - that will calculate the result of calling `evaluate(t, y)` on the given - expression tree (default is True) + convert_to_format : str + Whether to convert the expression trees representing the rhs and + algebraic equations, Jacobain (if using) and events into a different format: + + - None: keep PyBaMM expression tree structure. + - "python": convert into pure python code that will calculate the result of + calling `evaluate(t, y)` on the given expression treeself. + - "casadi": convert into CasADi expression tree, which then uses CasADi's + algorithm to calculate the Jacobian. + + Default is "python". """ def __init__(self, name="Unnamed model"): @@ -86,7 +92,7 @@ def __init__(self, name="Unnamed model"): # Default behaviour is to use the jacobian and simplify self.use_jacobian = True self.use_simplify = True - self.use_to_python = True + self.convert_to_format = "python" def _set_dictionary(self, dict, name): """ diff --git a/pybamm/solvers/algebraic_solver.py b/pybamm/solvers/algebraic_solver.py index baea9ebb5d..f3be30d360 100644 --- a/pybamm/solvers/algebraic_solver.py +++ b/pybamm/solvers/algebraic_solver.py @@ -199,14 +199,14 @@ def set_up(self, model): pybamm.logger.info("Simplifying jacobian") jac = jac.simplify() - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting jacobian to python") jac = pybamm.EvaluatorPython(jac) else: jac = None - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting algebraic to python") concatenated_algebraic = pybamm.EvaluatorPython(concatenated_algebraic) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index ad1ad2b4a8..1e1a0d0f4a 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -73,7 +73,10 @@ def solve(self, model, t_eval): # Set up timer = pybamm.Timer() start_time = timer.time() - self.set_up(model) + if model.convert_to_format == "casadi": + self.set_up_casadi(model) + else: + self.set_up(model) set_up_time = timer.time() - start_time # Solve @@ -127,7 +130,10 @@ def step(self, model, dt, npts=2): # Run set up on first step if not hasattr(self, "y0"): start_time = timer.time() - self.set_up(model) + if model.convert_to_format == "casadi": + self.set_up_casadi(model) + else: + self.set_up(model) self.t = 0.0 set_up_time = timer.time() - start_time else: @@ -187,11 +193,17 @@ def set_up(self, model): The model whose solution to calculate. Must have attributes rhs and initial_conditions - Raises - ------ - :class:`pybamm.SolverError` - If the model contains any algebraic equations (in which case a DAE solver - should be used instead) + """ + raise NotImplementedError + + def set_up_casadi(self, model): + """Convert model to casadi format and use their inbuilt functionalities. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions """ raise NotImplementedError diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 003403254f..656413c9d0 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -140,7 +140,7 @@ def set_up(self, model): jac_algebraic = simp.simplify(jac_algebraic) jac = simp.simplify(jac) - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting jacobian to python") jac_algebraic = pybamm.EvaluatorPython(jac_algebraic) jac = pybamm.EvaluatorPython(jac) @@ -152,7 +152,7 @@ def jac_alg_fn(t, y): jac = None jac_alg_fn = None - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting RHS to python") concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs) pybamm.logger.info("Converting algebraic to python") diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index db279b2577..cbe2514c99 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -99,13 +99,13 @@ def set_up(self, model): pybamm.logger.info("Simplifying jacobian") jac_rhs = simp.simplify(jac_rhs) - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting jacobian to python") jac_rhs = pybamm.EvaluatorPython(jac_rhs) else: jac_rhs = None - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting RHS to python") concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs) pybamm.logger.info("Converting events to python") @@ -145,6 +145,83 @@ def jacobian(t, y): self.event_funs = event_funs self.jacobian = jacobian + def set_up_casadi(self, model): + """Convert model to casadi format and use their inbuilt functionalities. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + + Raises + ------ + :class:`pybamm.SolverError` + If the model contains any algebraic equations (in which case a DAE solver + should be used instead) + + """ + # Check for algebraic equations + if len(model.algebraic) > 0: + raise pybamm.SolverError( + """Cannot use ODE solver to solve model with DAEs""" + ) + + # create simplified rhs and event expressions + concatenated_rhs = model.concatenated_rhs + events = model.events + + y0 = model.concatenated_initial_conditions[:, 0] + + t_casadi = casadi.SX.sym("t") + y_casadi = casadi.SX.sym("y", len(y0)) + pybamm.logger.info("Converting RHS to CasADi") + concatenated_rhs = concatenated_rhs.to_casadi(t_casadi, y_casadi) + pybamm.logger.info("Converting events to CasADi") + events = { + name: event.to_casadi(t_casadi, y_casadi) for name, event in events.items() + } + + # Create function to evaluate rhs + concatenated_rhs_fn = casadi.Function( + "rhs", [t_casadi, y_casadi], [concatenated_rhs] + ) + + def dydt(t, y): + pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) + dy = concatenated_rhs_fn(t, y).full() + return dy[:, 0] + + # Create event-dependent function to evaluate events + def event_fun(event): + event_fn = casadi.Function("event", [t_casadi, y_casadi], [event]) + + def eval_event(t, y): + return event_fun(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in events.values()] + + # Create function to evaluate jacobian + if model.use_jacobian: + + casadi_jac = casadi.jacobian() + casadi_jac_fn = casadi.Function("jac", [t_casadi, y_casadi], [casadi_jac]) + + def jacobian(t, y): + return casadi_jac_fn(t, y) + + else: + jacobian = None + + # Add the solver attributes + self.y0 = y0 + self.dydt = dydt + self.events = events + self.event_funs = event_funs + self.jacobian = jacobian + def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None ): diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 2b5afa1244..ba827e9cd3 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -172,6 +172,7 @@ def evaluate_model(self, simplify=False, use_known_evals=False, to_python=False) def set_up_model(self, simplify=False, to_python=False): self.model.use_simplify = simplify - self.model.use_to_python = to_python + if to_python is True: + self.model.convert_to_format = "python" self.model.default_solver.set_up(self.model) return None From 25ac27678c459ad3bb3ecd7e141ff798d4c89939 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 21 Oct 2019 13:50:22 -0400 Subject: [PATCH 037/122] #664 integrating casadi into solvers --- examples/scripts/compare_lead_acid.py | 7 +- examples/scripts/compare_lithium_ion.py | 7 +- pybamm/solvers/base_solver.py | 1 + pybamm/solvers/dae_solver.py | 127 ++++++++++++++++++- pybamm/solvers/ode_solver.py | 15 ++- pybamm/solvers/scipy_solver.py | 2 +- tests/unit/test_solvers/test_scipy_solver.py | 24 ++++ 7 files changed, 164 insertions(+), 19 deletions(-) diff --git a/examples/scripts/compare_lead_acid.py b/examples/scripts/compare_lead_acid.py index dc02b534dc..468d816bd0 100644 --- a/examples/scripts/compare_lead_acid.py +++ b/examples/scripts/compare_lead_acid.py @@ -18,9 +18,9 @@ # load models models = [ pybamm.lead_acid.LOQS(), - pybamm.lead_acid.FOQS(), - pybamm.lead_acid.Composite(), - pybamm.lead_acid.Full(), + # pybamm.lead_acid.FOQS(), + # pybamm.lead_acid.Composite(), + # pybamm.lead_acid.Full(), ] # load parameter values and process models and geometry @@ -43,6 +43,7 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 3, 1000) for i, model in enumerate(models): + model.convert_to_format = "casadi" solution = model.default_solver.solve(model, t_eval) solutions[i] = solution diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 7fcfae8788..42056e5c5b 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -20,7 +20,7 @@ models = [ pybamm.lithium_ion.SPM(options), pybamm.lithium_ion.SPMe(options), - pybamm.lithium_ion.DFN(options), + # pybamm.lithium_ion.DFN(options), ] @@ -32,7 +32,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} +var_pts = {var.x_n: 100, var.x_s: 100, var.x_p: 100, var.r_n: 50, var.r_p: 50} # discretise models for model in models: @@ -47,7 +47,8 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): - solutions[i] = pybamm.CasadiSolver().solve(model, t_eval) + # model.convert_to_format = "casadi" + solutions[i] = model.default_solver.solve(model, t_eval) # plot plot = pybamm.QuickPlot(models, mesh, solutions) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 1e1a0d0f4a..6674544bd8 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -1,6 +1,7 @@ # # Base solver class # +import casadi import pybamm import numpy as np diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 656413c9d0..5d19357b6e 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -102,12 +102,6 @@ def set_up(self, model): model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions - - Raises - ------ - :class:`pybamm.SolverError` - If the model contains any algebraic equations (in which case a DAE solver - should be used instead) """ # create simplified rhs, algebraic and event expressions concatenated_rhs = model.concatenated_rhs @@ -220,6 +214,127 @@ def jacobian(t, y): self.event_funs = event_funs self.jacobian = jacobian + def set_up_casadi(self, model): + """Convert model to casadi format and use their inbuilt functionalities. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + """ + # create simplified rhs, algebraic and event expressions + concatenated_rhs = model.concatenated_rhs + concatenated_algebraic = model.concatenated_algebraic + events = model.events + + t_casadi = casadi.SX.sym("t") + y_casadi = casadi.SX.sym("y", len(y0)) + pybamm.logger.info("Converting RHS to CasADi") + concatenated_rhs = concatenated_rhs.to_casadi(t_casadi, y_casadi) + concatenated_algebraic = concatenated_algebraic.to_casadi(t_casadi, y_casadi) + if model.use_simplify: + simp = pybamm.Simplification() + pybamm.logger.info("Simplifying events") + events = {name: simp.simplify(event) for name, event in events.items()} + # pybamm.logger.info("Converting events to python") + # events = {name: pybamm.EvaluatorPython(event) for name, event in events.items()} + + # Create functions to evaluate rhs and algebraic + concatenated_rhs_fn = casadi.Function( + "rhs", [t_casadi, y_casadi], [concatenated_rhs] + ) + concatenated_algebraic_fn = casadi.Function( + "algebraic", [t_casadi, y_casadi], [concatenated_algebraic] + ) + + def dydt(t, y): + pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) + dy = concatenated_rhs_fn(t, y).full() + return dy[:, 0] + + if model.use_jacobian: + + casadi_jac = casadi.jacobian(concatenated_rhs, y_casadi) + casadi_jac_fn = casadi.Function( + "jacobian", [t_casadi, y_casadi], [casadi_jac] + ) + + def jacobian(t, y): + return casadi_jac_fn(t, y) + + else: + jacobian = None + jacobian_alg = None + + if model.convert_to_format == "python": + pybamm.logger.info("Converting RHS to python") + concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs) + pybamm.logger.info("Converting algebraic to python") + concatenated_algebraic = pybamm.EvaluatorPython(concatenated_algebraic) + pybamm.logger.info("Converting events to python") + events = { + name: pybamm.EvaluatorPython(event) for name, event in events.items() + } + + # Calculate consistent initial conditions for the algebraic equations + def rhs(t, y): + return concatenated_rhs.evaluate(t, y, known_evals={})[0][:, 0] + + def algebraic(t, y): + return concatenated_algebraic.evaluate(t, y, known_evals={})[0][:, 0] + + if len(model.algebraic) > 0: + y0 = self.calculate_consistent_initial_conditions( + rhs, algebraic, model.concatenated_initial_conditions[:, 0], jac_alg_fn + ) + else: + # can use DAE solver to solve ODE model + y0 = model.concatenated_initial_conditions[:, 0] + + # Create functions to evaluate residuals + def residuals(t, y, ydot): + pybamm.logger.debug( + "Evaluating residuals for {} at t={}".format(model.name, t) + ) + y = y[:, np.newaxis] + rhs_eval, known_evals = concatenated_rhs.evaluate(t, y, known_evals={}) + # reuse known_evals + alg_eval = concatenated_algebraic.evaluate(t, y, known_evals=known_evals)[0] + # turn into 1D arrays + rhs_eval = rhs_eval[:, 0] + alg_eval = alg_eval[:, 0] + return ( + np.concatenate((rhs_eval, alg_eval)) - model.mass_matrix.entries @ ydot + ) + + # Create event-dependent function to evaluate events + def event_fun(event): + def eval_event(t, y): + return event.evaluate(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in events.values()] + + # Create function to evaluate jacobian + if jac is not None: + + def jacobian(t, y): + return jac.evaluate(t, y, known_evals={})[0] + + else: + jacobian = None + + # Add the solver attributes + self.y0 = y0 + self.rhs = rhs + self.algebraic = algebraic + self.residuals = residuals + self.events = events + self.event_funs = event_funs + self.jacobian = jacobian + def calculate_consistent_initial_conditions( self, rhs, algebraic, y0_guess, jac=None ): diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index cbe2514c99..455f06ed5b 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -1,6 +1,7 @@ # # Base solver class # +import casadi import pybamm import numpy as np @@ -178,7 +179,7 @@ def set_up_casadi(self, model): pybamm.logger.info("Converting RHS to CasADi") concatenated_rhs = concatenated_rhs.to_casadi(t_casadi, y_casadi) pybamm.logger.info("Converting events to CasADi") - events = { + casadi_events = { name: event.to_casadi(t_casadi, y_casadi) for name, event in events.items() } @@ -194,20 +195,22 @@ def dydt(t, y): # Create event-dependent function to evaluate events def event_fun(event): - event_fn = casadi.Function("event", [t_casadi, y_casadi], [event]) + casadi_event_fn = casadi.Function("event", [t_casadi, y_casadi], [event]) def eval_event(t, y): - return event_fun(t, y) + return casadi_event_fn(t, y) return eval_event - event_funs = [event_fun(event) for event in events.values()] + event_funs = [event_fun(event) for event in casadi_events.values()] # Create function to evaluate jacobian if model.use_jacobian: - casadi_jac = casadi.jacobian() - casadi_jac_fn = casadi.Function("jac", [t_casadi, y_casadi], [casadi_jac]) + casadi_jac = casadi.jacobian(concatenated_rhs, y_casadi) + casadi_jac_fn = casadi.Function( + "jacobian", [t_casadi, y_casadi], [casadi_jac] + ) def jacobian(t, y): return casadi_jac_fn(t, y) diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index fd7afbf498..23e11483c5 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -83,7 +83,7 @@ def integrate( termination = "event" t_event = [] for time in sol.t_events: - if time: + if time.size > 0: t_event = np.append(t_event, np.max(time)) t_event = np.array([np.max(t_event)]) y_event = sol.sol(t_event) diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index cef29c65e0..58ad303efb 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -270,6 +270,30 @@ def test_model_step(self): solution = solver.solve(model, t_eval) np.testing.assert_allclose(solution.y[0], step_sol.y[0]) + def test_model_solver_with_event_with_casadi(self): + # Create model + model = pybamm.BaseModel() + model.convert_to_format = "casadi" + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -0.1 * var} + model.initial_conditions = {var: 1} + model.events = {"var=0.5": pybamm.min(var - 0.5)} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval) + self.assertLess(len(solution.t), len(t_eval)) + np.testing.assert_array_equal(solution.t, t_eval[: len(solution.t)]) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t)) + if __name__ == "__main__": print("Add -v for more debug output") From 5a1922e55e5f25896018cb583876039cb61b2882 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 21 Oct 2019 21:57:42 -0400 Subject: [PATCH 038/122] #664 set up dae solver --- examples/scripts/compare_lithium_ion.py | 6 +- pybamm/solvers/dae_solver.py | 117 +++++++----------- pybamm/solvers/ode_solver.py | 11 +- .../unit/test_solvers/test_scikits_solvers.py | 49 ++++++++ 4 files changed, 104 insertions(+), 79 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 42056e5c5b..45c08bd699 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -20,7 +20,7 @@ models = [ pybamm.lithium_ion.SPM(options), pybamm.lithium_ion.SPMe(options), - # pybamm.lithium_ion.DFN(options), + pybamm.lithium_ion.DFN(options), ] @@ -32,7 +32,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 100, var.x_s: 100, var.x_p: 100, var.r_n: 50, var.r_p: 50} +var_pts = {var.x_n: 20, var.x_s: 20, var.x_p: 20, var.r_n: 10, var.r_p: 10} # discretise models for model in models: @@ -47,7 +47,7 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): - # model.convert_to_format = "casadi" + model.convert_to_format = "casadi" solutions[i] = model.default_solver.solve(model, t_eval) # plot diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 5d19357b6e..846abf64ae 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -1,6 +1,7 @@ # # Base solver class # +import casadi import pybamm import numpy as np from scipy import optimize @@ -139,12 +140,15 @@ def set_up(self, model): jac_algebraic = pybamm.EvaluatorPython(jac_algebraic) jac = pybamm.EvaluatorPython(jac) - def jac_alg_fn(t, y): - return jac_algebraic.evaluate(t, y) + def jacobian(t, y): + return jac.evaluate(t, y, known_evals={})[0] + + def jacobian_alg(t, y): + return jac_algebraic.evaluate(t, y, known_evals={})[0] else: - jac = None - jac_alg_fn = None + jacobian = None + jacobian_alg = None if model.convert_to_format == "python": pybamm.logger.info("Converting RHS to python") @@ -165,7 +169,10 @@ def algebraic(t, y): if len(model.algebraic) > 0: y0 = self.calculate_consistent_initial_conditions( - rhs, algebraic, model.concatenated_initial_conditions[:, 0], jac_alg_fn + rhs, + algebraic, + model.concatenated_initial_conditions[:, 0], + jacobian_alg, ) else: # can use DAE solver to solve ODE model @@ -196,15 +203,6 @@ def eval_event(t, y): event_funs = [event_fun(event) for event in events.values()] - # Create function to evaluate jacobian - if jac is not None: - - def jacobian(t, y): - return jac.evaluate(t, y, known_evals={})[0] - - else: - jacobian = None - # Add the solver attributes self.y0 = y0 self.rhs = rhs @@ -223,22 +221,21 @@ def set_up_casadi(self, model): The model whose solution to calculate. Must have attributes rhs and initial_conditions """ - # create simplified rhs, algebraic and event expressions - concatenated_rhs = model.concatenated_rhs - concatenated_algebraic = model.concatenated_algebraic - events = model.events - + # Convert model attributes to casadi t_casadi = casadi.SX.sym("t") - y_casadi = casadi.SX.sym("y", len(y0)) + y_casadi = casadi.SX.sym("y", len(model.concatenated_initial_conditions)) pybamm.logger.info("Converting RHS to CasADi") - concatenated_rhs = concatenated_rhs.to_casadi(t_casadi, y_casadi) - concatenated_algebraic = concatenated_algebraic.to_casadi(t_casadi, y_casadi) - if model.use_simplify: - simp = pybamm.Simplification() - pybamm.logger.info("Simplifying events") - events = {name: simp.simplify(event) for name, event in events.items()} - # pybamm.logger.info("Converting events to python") - # events = {name: pybamm.EvaluatorPython(event) for name, event in events.items()} + concatenated_rhs = model.concatenated_rhs.to_casadi(t_casadi, y_casadi) + pybamm.logger.info("Converting algebraic to CasADi") + concatenated_algebraic = model.concatenated_algebraic.to_casadi( + t_casadi, y_casadi + ) + all_states = casadi.vertcat(concatenated_rhs, concatenated_algebraic) + pybamm.logger.info("Converting events to CasADi") + casadi_events = { + name: event.to_casadi(t_casadi, y_casadi) + for name, event in model.events.items() + } # Create functions to evaluate rhs and algebraic concatenated_rhs_fn = casadi.Function( @@ -247,91 +244,73 @@ def set_up_casadi(self, model): concatenated_algebraic_fn = casadi.Function( "algebraic", [t_casadi, y_casadi], [concatenated_algebraic] ) - - def dydt(t, y): - pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) - dy = concatenated_rhs_fn(t, y).full() - return dy[:, 0] + all_states_fn = casadi.Function("all", [t_casadi, y_casadi], [all_states]) if model.use_jacobian: - casadi_jac = casadi.jacobian(concatenated_rhs, y_casadi) + casadi_jac = casadi.jacobian(all_states, y_casadi) casadi_jac_fn = casadi.Function( "jacobian", [t_casadi, y_casadi], [casadi_jac] ) + casadi_jac_alg = casadi.jacobian(concatenated_algebraic, y_casadi) + casadi_jac_alg_fn = casadi.Function( + "jacobian", [t_casadi, y_casadi], [casadi_jac_alg] + ) def jacobian(t, y): return casadi_jac_fn(t, y) + def jacobian_alg(t, y): + return casadi_jac_alg_fn(t, y) + else: jacobian = None jacobian_alg = None - if model.convert_to_format == "python": - pybamm.logger.info("Converting RHS to python") - concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs) - pybamm.logger.info("Converting algebraic to python") - concatenated_algebraic = pybamm.EvaluatorPython(concatenated_algebraic) - pybamm.logger.info("Converting events to python") - events = { - name: pybamm.EvaluatorPython(event) for name, event in events.items() - } - # Calculate consistent initial conditions for the algebraic equations def rhs(t, y): - return concatenated_rhs.evaluate(t, y, known_evals={})[0][:, 0] + return concatenated_rhs_fn(t, y).full()[:, 0] def algebraic(t, y): - return concatenated_algebraic.evaluate(t, y, known_evals={})[0][:, 0] + return concatenated_algebraic_fn(t, y).full()[:, 0] if len(model.algebraic) > 0: y0 = self.calculate_consistent_initial_conditions( - rhs, algebraic, model.concatenated_initial_conditions[:, 0], jac_alg_fn + rhs, + algebraic, + model.concatenated_initial_conditions[:, 0], + jacobian_alg, ) else: # can use DAE solver to solve ODE model y0 = model.concatenated_initial_conditions[:, 0] # Create functions to evaluate residuals + def residuals(t, y, ydot): pybamm.logger.debug( "Evaluating residuals for {} at t={}".format(model.name, t) ) - y = y[:, np.newaxis] - rhs_eval, known_evals = concatenated_rhs.evaluate(t, y, known_evals={}) - # reuse known_evals - alg_eval = concatenated_algebraic.evaluate(t, y, known_evals=known_evals)[0] - # turn into 1D arrays - rhs_eval = rhs_eval[:, 0] - alg_eval = alg_eval[:, 0] - return ( - np.concatenate((rhs_eval, alg_eval)) - model.mass_matrix.entries @ ydot - ) + states_eval = all_states_fn(t, y).full()[:, 0] + return states_eval - model.mass_matrix.entries @ ydot # Create event-dependent function to evaluate events def event_fun(event): + casadi_event_fn = casadi.Function("event", [t_casadi, y_casadi], [event]) + def eval_event(t, y): - return event.evaluate(t, y) + return casadi_event_fn(t, y) return eval_event - event_funs = [event_fun(event) for event in events.values()] - - # Create function to evaluate jacobian - if jac is not None: - - def jacobian(t, y): - return jac.evaluate(t, y, known_evals={})[0] - - else: - jacobian = None + event_funs = [event_fun(event) for event in casadi_events.values()] # Add the solver attributes self.y0 = y0 self.rhs = rhs self.algebraic = algebraic self.residuals = residuals - self.events = events + self.events = model.events self.event_funs = event_funs self.jacobian = jacobian diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index 455f06ed5b..386c6b4c7a 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -168,19 +168,16 @@ def set_up_casadi(self, model): """Cannot use ODE solver to solve model with DAEs""" ) - # create simplified rhs and event expressions - concatenated_rhs = model.concatenated_rhs - events = model.events - y0 = model.concatenated_initial_conditions[:, 0] t_casadi = casadi.SX.sym("t") y_casadi = casadi.SX.sym("y", len(y0)) pybamm.logger.info("Converting RHS to CasADi") - concatenated_rhs = concatenated_rhs.to_casadi(t_casadi, y_casadi) + concatenated_rhs = model.concatenated_rhs.to_casadi(t_casadi, y_casadi) pybamm.logger.info("Converting events to CasADi") casadi_events = { - name: event.to_casadi(t_casadi, y_casadi) for name, event in events.items() + name: event.to_casadi(t_casadi, y_casadi) + for name, event in model.events.items() } # Create function to evaluate rhs @@ -221,7 +218,7 @@ def jacobian(t, y): # Add the solver attributes self.y0 = y0 self.dydt = dydt - self.events = events + self.events = model.events self.event_funs = event_funs self.jacobian = jacobian diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index 36b3cb21f8..ce4d384acd 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -680,6 +680,55 @@ def test_model_step_dae(self): np.testing.assert_allclose(solution.y[0], step_sol.y[0, :]) np.testing.assert_allclose(solution.y[-1], step_sol.y[-1, :]) + def test_model_solver_ode_events_casadi(self): + # Create model + model = pybamm.BaseModel() + model.convert_to_format = "casadi" + whole_cell = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=whole_cell) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + model.events = { + "2 * var = 2.5": pybamm.min(2 * var - 2.5), + "var = 1.5": pybamm.min(var - 1.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScikitsOdeSolver(rtol=1e-9, atol=1e-9) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[0], 1.25) + + def test_model_solver_dae_events_casadi(self): + # Create model + model = pybamm.BaseModel() + model.convert_to_format = "casadi" + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = { + "var1 = 1.5": pybamm.min(var1 - 1.5), + "var2 = 2.5": pybamm.min(var2 - 2.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + if __name__ == "__main__": print("Add -v for more debug output") From c4bda5645db0c04443a498de7490fced6629b1c1 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Mon, 21 Oct 2019 23:19:01 -0400 Subject: [PATCH 039/122] #664 set up casadi solver for odes and add tests --- .../full_battery_models/lithium_ion/spm.py | 3 +- .../full_battery_models/lithium_ion/spme.py | 3 +- pybamm/solvers/casadi_solver.py | 151 ++++++++---------- pybamm/solvers/dae_solver.py | 3 + tests/unit/test_solvers/test_casadi_solver.py | 129 +++++++++++++++ 5 files changed, 199 insertions(+), 90 deletions(-) create mode 100644 tests/unit/test_solvers/test_casadi_solver.py diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index c8a9549127..8d4ce0302d 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -120,6 +120,7 @@ def default_solver(self): # Different solver depending on whether we solve ODEs or DAEs dimensionality = self.options["dimensionality"] if dimensionality == 0: - return pybamm.ScipySolver() + return pybamm.CasadiSolver() + # return pybamm.ScipySolver() else: return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py index e5e6167592..7812f87856 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spme.py +++ b/pybamm/models/full_battery_models/lithium_ion/spme.py @@ -124,6 +124,7 @@ def default_solver(self): # Different solver depending on whether we solve ODEs or DAEs dimensionality = self.options["dimensionality"] if dimensionality == 0: - return pybamm.ScipySolver() + return pybamm.CasadiSolver() + # return pybamm.ScipySolver() else: return pybamm.ScikitsDaeSolver() diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index d8a6b02c92..d4b828ead5 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -1,11 +1,12 @@ # -# Wrap CasADi +# CasADi Solver class # -import pybamm import casadi +import pybamm +import numpy as np -class CasadiSolver(pybamm.BaseSolver): +class CasadiSolver(pybamm.DaeSolver): """Solve a discretised model, using CasADi. Parameters @@ -14,13 +15,25 @@ class CasadiSolver(pybamm.BaseSolver): The relative tolerance for the solver (default is 1e-6). atol : float, optional The absolute tolerance for the solver (default is 1e-6). + + **Extends**: :class:`pybamm.DaeSolver` """ - def __init__(self, method=None, rtol=1e-6, atol=1e-6): - super().__init__(method, rtol, atol) + def __init__( + self, + method="idas", + rtol=1e-6, + atol=1e-6, + root_method="lm", + root_tol=1e-6, + max_steps=1000, + ): + super().__init__(method, rtol, atol, root_method, root_tol, max_steps) def compute_solution(self, model, t_eval): - """Calculate the solution of the model at specified times. + """Calculate the solution of the model at specified times. In this class, we + overwrite the behaviour of :class:`pybamm.DaeSolver`, since CasADi requires + slightly different syntax. Parameters ---------- @@ -31,17 +44,19 @@ def compute_solution(self, model, t_eval): The times at which to compute the solution """ + # Convert to CasADi if not already done + if not hasattr(self, "casadi_problem"): + pybamm.logger.info( + "Converting model to CasADi format, required for CasADi solver" + ) + self.set_up_casadi(model) + timer = pybamm.Timer() solve_start_time = timer.time() - pybamm.logger.info("Calling ODE solver") - solution = self.integrate( - self.dydt, - self.y0, - t_eval, - events=self.event_funs, - mass_matrix=model.mass_matrix.entries, - jacobian=self.jacobian, + pybamm.logger.info("Calling DAE solver") + solution = self.integrate_casadi( + self.casadi_problem, self.y0, t_eval, mass_matrix=model.mass_matrix.entries ) solve_time = timer.time() - solve_start_time @@ -50,87 +65,47 @@ def compute_solution(self, model, t_eval): return solution, solve_time, termination - def set_up(self, model): - """Unpack model, perform checks, simplify and calculate jacobian. - - Parameters - ---------- - model : :class:`pybamm.BaseModel` - The model whose solution to calculate. Must have attributes rhs and - initial_conditions - - Raises - ------ - :class:`pybamm.SolverError` - If the model contains any algebraic equations (in which case a DAE solver - should be used instead) - - """ - y0 = model.concatenated_initial_conditions[:, 0] - - t = casadi.SX.sym("t") - y_diff = casadi.SX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) - y_alg = casadi.SX.sym( - "y_alg", len(model.concatenated_algebraic.evaluate(0, y0)) - ) - y = casadi.vertcat(y_diff, y_alg) - # create simplified rhs and event expressions - concatenated_rhs = model.concatenated_rhs.to_casadi(t, y) - concatenated_algebraic = model.concatenated_algebraic.to_casadi(t, y) - events = {name: event.to_casadi(t, y) for name, event in model.events.items()} - - # Create function to evaluate rhs - def dydt(t, y): - pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) - y = y[:, np.newaxis] - dy = concatenated_rhs.evaluate(t, y, known_evals={})[0] - return dy[:, 0] - - # Create event-dependent function to evaluate events - def event_fun(event): - def eval_event(t, y): - return event.evaluate(t, y) - - return eval_event - - event_funs = [event_fun(event) for event in events.values()] - - # Create function to evaluate jacobian - if jac_rhs is not None: - - def jacobian(t, y): - return jac_rhs.evaluate(t, y, known_evals={})[0] - - else: - jacobian = None - - # Add the solver attributes - self.y0 = y0 - self.dydt = dydt - self.events = events - self.event_funs = event_funs - self.jacobian = jacobian - - def integrate( - self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None - ): + def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): """ - Solve a model defined by dydt with initial conditions y0. + Solve a DAE model defined by residuals with initial conditions y0. Parameters ---------- - derivs : method - A function that takes in t and y and returns the time-derivative dydt + residuals : method + A function that takes in t, y and ydot and returns the residuals of the + equations y0 : numeric type The initial conditions t_eval : numeric type The times at which to compute the solution - events : method, optional - A function that takes in t and y and returns conditions for the solver to - stop mass_matrix : array_like, optional - The (sparse) mass matrix for the chosen spatial method. - jacobian : method, optional - A function that takes in t and y and returns the Jacobian + The (sparse) mass matrix for the chosen spatial method. This is only passed + to check that the mass matrix is diagonal with 1s for the odes and 0s for + the algebraic equations, as CasADi does not allow to pass mass matrices. """ - raise NotImplementedError + options = { + "grid": t_eval, + "reltol": self.rtol, + "abstol": self.atol, + "output_t0": True, + } + if self.method == "idas": + options["calc_ic"] = True + + # set up and solve + integrator = casadi.integrator("F", self.method, problem, options) + try: + # return solution + sol = integrator(x0=y0) + y_values = np.concatenate([sol["xf"].full(), sol["zf"].full()]) + return pybamm.Solution(t_eval, y_values, None, None, "final time") + except RuntimeError as e: + raise pybamm.SolverError(e.args[0]) + + def set_up(self, model): + "Skip classic set up with this solver" + pass + + # def calculate_consistent_initial_conditions(self): + # "No need to calculate initial conditions separately with this solver" + # pass diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 846abf64ae..efd5431df2 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -314,6 +314,9 @@ def eval_event(t, y): self.event_funs = event_funs self.jacobian = jacobian + # Create CasADi problem for the CasADi solver + self.casadi_problem = {"x": y_casadi, "ode": concatenated_rhs} + def calculate_consistent_initial_conditions( self, rhs, algebraic, y0_guess, jac=None ): diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py new file mode 100644 index 0000000000..cfed566600 --- /dev/null +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -0,0 +1,129 @@ +# +# Tests for the Casadi Solver class +# +import casadi +import pybamm +import unittest +import numpy as np +from tests import get_mesh_for_testing +import warnings + + +class TestCasadiSolver(unittest.TestCase): + def test_integrate(self): + # Constant + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + + y = casadi.SX.sym("y") + constant_growth = casadi.SX(0.5) + problem = {"x": y, "ode": constant_growth} + + y0 = np.array([0]) + t_eval = np.linspace(0, 1, 100) + solution = solver.integrate_casadi(problem, y0, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(0.5 * solution.t, solution.y[0]) + + # Exponential decay + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="cvodes") + + exponential_decay = -0.1 * y + problem = {"x": y, "ode": exponential_decay} + + y0 = np.array([1]) + t_eval = np.linspace(0, 1, 100) + solution = solver.integrate_casadi(problem, y0, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t)) + self.assertEqual(solution.termination, "final time") + + def test_integrate_failure(self): + # Turn off warnings to ignore sqrt error + warnings.simplefilter("ignore") + + y = casadi.SX.sym("y") + sqrt_decay = -np.sqrt(y) + + y0 = np.array([1]) + t_eval = np.linspace(0, 3, 100) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + problem = {"x": y, "ode": sqrt_decay} + # Expect solver to fail when y goes negative + with self.assertRaises(pybamm.SolverError): + solver.integrate_casadi(problem, y0, t_eval) + + # Turn warnings back on + warnings.simplefilter("default") + + def test_model_solver(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + + # Test time + self.assertGreater( + solution.total_time, solution.solve_time + solution.set_up_time + ) + + # + # def test_model_step(self): + # # Create model + # model = pybamm.BaseModel() + # domain = ["negative electrode", "separator", "positive electrode"] + # var = pybamm.Variable("var", domain=domain) + # model.rhs = {var: 0.1 * var} + # model.initial_conditions = {var: 1} + # # No need to set parameters; can use base discretisation (no spatial operators) + # + # # create discretisation + # mesh = get_mesh_for_testing() + # spatial_methods = {"macroscale": pybamm.FiniteVolume} + # disc = pybamm.Discretisation(mesh, spatial_methods) + # disc.process_model(model) + # + # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + # + # # Step once + # dt = 0.1 + # step_sol = solver.step(model, dt) + # np.testing.assert_array_equal(step_sol.t, [0, dt]) + # np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + # + # # Step again (return 5 points) + # step_sol_2 = solver.step(model, dt, npts=5) + # np.testing.assert_array_equal(step_sol_2.t, np.linspace(dt, 2 * dt, 5)) + # np.testing.assert_allclose(step_sol_2.y[0], np.exp(0.1 * step_sol_2.t)) + # + # # append solutions + # step_sol.append(step_sol_2) + # + # # Check steps give same solution as solve + # t_eval = step_sol.t + # solution = solver.solve(model, t_eval) + # np.testing.assert_allclose(solution.y[0], step_sol.y[0]) + # + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() From cbf97b7e6baec7d782f00cee0cee3065b5138a01 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 22 Oct 2019 23:06:26 -0400 Subject: [PATCH 040/122] #664 fix bugs with function diff and converting autograd to casadi --- CHANGELOG.md | 17 +++-- examples/scripts/compare_lead_acid.py | 10 +-- examples/scripts/compare_lithium_ion.py | 2 +- examples/scripts/compare_lithium_ion_3D.py | 7 +- pybamm/expression_tree/functions.py | 65 ++++++++++++----- .../operations/convert_to_casadi.py | 18 ++++- .../full_battery_models/lithium_ion/spm.py | 3 +- .../full_battery_models/lithium_ion/spme.py | 3 +- pybamm/solvers/casadi_solver.py | 20 ++++-- pybamm/solvers/dae_solver.py | 15 +++- pybamm/solvers/ode_solver.py | 1 + .../test_expression_tree/test_functions.py | 22 +++++- .../test_operations/test_convert_to_casadi.py | 24 ++++++- tests/unit/test_solvers/test_casadi_solver.py | 72 +++++++++---------- tests/unit/test_solvers/test_scipy_solver.py | 3 + 15 files changed, 196 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb312ad6ee..352a1a4a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,23 @@ ## Features -- Add `Interpolant` class to interpolate experimental data (e.g. OCP curves) (#661) -- Allow parameters to be set by material or by specifying a particular paper (#647) -- Set relative and absolute tolerances independently in solvers (#645) -- Add some non-uniform meshes in 1D and 2D (#617) +- Add interface to CasADi solver +- Add option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians +- Add `Interpolant` class to interpolate experimental data (e.g. OCP curves) ([#661](https://github.com/pybamm-team/PyBaMM/pull/661)) +- Allow parameters to be set by material or by specifying a particular paper ([#647](https://github.com/pybamm-team/PyBaMM/pull/647)) +- Set relative and absolute tolerances independently in solvers ([#645](https://github.com/pybamm-team/PyBaMM/pull/645)) +- Add some non-uniform meshes in 1D and 2D ([#617](https://github.com/pybamm-team/PyBaMM/pull/617)) ## Optimizations -- Avoid re-checking size when making a copy of an `Index` object (#656) -- Avoid recalculating `_evaluation_array` when making a copy of a `StateVector` object (#653) +- Avoid re-checking size when making a copy of an `Index` object ([#656](https://github.com/pybamm-team/PyBaMM/pull/656)) +- Avoid recalculating `_evaluation_array` when making a copy of a `StateVector` object ([#653](https://github.com/pybamm-team/PyBaMM/pull/653)) ## Bug fixes +- Fix differentiation of functions that have more than one argument + + # [v0.1.0](https://github.com/pybamm-team/PyBaMM/tree/v0.1.0) - 2019-10-08 This is the first official version of PyBaMM. diff --git a/examples/scripts/compare_lead_acid.py b/examples/scripts/compare_lead_acid.py index 468d816bd0..a10669af3f 100644 --- a/examples/scripts/compare_lead_acid.py +++ b/examples/scripts/compare_lead_acid.py @@ -18,9 +18,9 @@ # load models models = [ pybamm.lead_acid.LOQS(), - # pybamm.lead_acid.FOQS(), - # pybamm.lead_acid.Composite(), - # pybamm.lead_acid.Full(), + pybamm.lead_acid.FOQS(), + pybamm.lead_acid.Composite(), + pybamm.lead_acid.Full(), ] # load parameter values and process models and geometry @@ -41,10 +41,10 @@ # solve model solutions = [None] * len(models) -t_eval = np.linspace(0, 3, 1000) +t_eval = np.linspace(0, 0.66, 1000) for i, model in enumerate(models): model.convert_to_format = "casadi" - solution = model.default_solver.solve(model, t_eval) + solution = pybamm.CasadiSolver().solve(model, t_eval) solutions[i] = solution # plot diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 45c08bd699..fc06298566 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -32,7 +32,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 20, var.x_s: 20, var.x_p: 20, var.r_n: 10, var.r_p: 10} +var_pts = {var.x_n: 50, var.x_s: 50, var.x_p: 50, var.r_n: 50, var.r_p: 50} # discretise models for model in models: diff --git a/examples/scripts/compare_lithium_ion_3D.py b/examples/scripts/compare_lithium_ion_3D.py index e62fc7697b..9449f2e521 100644 --- a/examples/scripts/compare_lithium_ion_3D.py +++ b/examples/scripts/compare_lithium_ion_3D.py @@ -18,12 +18,10 @@ # load models models = [ pybamm.lithium_ion.SPM( - {"current collector": "potential pair", "dimensionality": 2}, - name="2+1D SPM", + {"current collector": "potential pair", "dimensionality": 2}, name="2+1D SPM" ), pybamm.lithium_ion.SPMe( - {"current collector": "potential pair", "dimensionality": 2}, - name="2+1D SPMe", + {"current collector": "potential pair", "dimensionality": 2}, name="2+1D SPMe" ), ] @@ -54,6 +52,7 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 1, 1000) for i, model in enumerate(models): + model.convert_to_format = "casadi" solution = model.default_solver.solve(model, t_eval) solutions[i] = solution diff --git a/pybamm/expression_tree/functions.py b/pybamm/expression_tree/functions.py index 20f6ae46e4..3bba9f8f6d 100644 --- a/pybamm/expression_tree/functions.py +++ b/pybamm/expression_tree/functions.py @@ -21,10 +21,19 @@ class Function(pybamm.Symbol): derivative : str, optional Which derivative to use when differentiating ("autograd" or "derivative"). Default is "autograd". + differentiated_function : method, optional + The function which was differentiated to obtain this one. Default is None. **Extends:** :class:`pybamm.Symbol` """ - def __init__(self, function, *children, name=None, derivative="autograd"): + def __init__( + self, + function, + *children, + name=None, + derivative="autograd", + differentiated_function=None + ): if name is not None: self.name = name @@ -38,6 +47,7 @@ def __init__(self, function, *children, name=None, derivative="autograd"): self.function = function self.derivative = derivative + self.differentiated_function = differentiated_function # hack to work out whether function takes any params # (signature doesn't work for numpy) @@ -79,7 +89,9 @@ def diff(self, variable): # if variable appears in the function,use autograd to differentiate # function, and apply chain rule if variable.id in [symbol.id for symbol in child.pre_order()]: - partial_derivatives[i] = self._diff(children) * child.diff(variable) + partial_derivatives[i] = self._diff(children, i) * child.diff( + variable + ) # remove None entries partial_derivatives = list(filter(None, partial_derivatives)) @@ -90,15 +102,31 @@ def diff(self, variable): return derivative - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Symbol._diff()`. """ + # Store differentiated function, needed in case we want to convert to CasADi if self.derivative == "autograd": - return Function(autograd.elementwise_grad(self.function), *children) - elif self.derivative == "derivative": - # keep using "derivative" as derivative - return pybamm.Function( - self.function.derivative(), *children, derivative="derivative" + return Function( + autograd.elementwise_grad(self.function, idx), + *children, + differentiated_function=self.function ) + elif self.derivative == "derivative": + if len(children) > 1: + raise ValueError( + """ + differentiation using '.derivative()' not implemented for functions + with more than one child + """ + ) + else: + # keep using "derivative" as derivative + return pybamm.Function( + self.function.derivative(), + *children, + derivative="derivative", + differentiated_function=self.function + ) def _jac(self, variable): """ See :meth:`pybamm.Symbol._jac()`. """ @@ -171,7 +199,11 @@ def _function_new_copy(self, children): A new copy of the function """ return pybamm.Function( - self.function, *children, name=self.name, derivative=self.derivative + self.function, + *children, + name=self.name, + derivative=self.derivative, + differentiated_function=self.differentiated_function ) def _function_simplify(self, simplified_children): @@ -199,7 +231,8 @@ def _function_simplify(self, simplified_children): self.function, *simplified_children, name=self.name, - derivative=self.derivative + derivative=self.derivative, + differentiated_function=self.differentiated_function ) @@ -235,7 +268,7 @@ class Cos(SpecificFunction): def __init__(self, child): super().__init__(np.cos, child) - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Symbol._diff()`. """ return -Sin(children[0]) @@ -251,7 +284,7 @@ class Cosh(SpecificFunction): def __init__(self, child): super().__init__(np.cosh, child) - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Function._diff()`. """ return Sinh(children[0]) @@ -267,7 +300,7 @@ class Exponential(SpecificFunction): def __init__(self, child): super().__init__(np.exp, child) - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Function._diff()`. """ return Exponential(children[0]) @@ -283,7 +316,7 @@ class Log(SpecificFunction): def __init__(self, child): super().__init__(np.log, child) - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Function._diff()`. """ return 1 / children[0] @@ -309,7 +342,7 @@ class Sin(SpecificFunction): def __init__(self, child): super().__init__(np.sin, child) - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Function._diff()`. """ return Cos(children[0]) @@ -325,7 +358,7 @@ class Sinh(SpecificFunction): def __init__(self, child): super().__init__(np.sinh, child) - def _diff(self, children): + def _diff(self, children, idx): """ See :meth:`pybamm.Function._diff()`. """ return Cosh(children[0]) diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 0f1a0d02bf..395bb6ec78 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -67,13 +67,29 @@ def _convert(self, symbol, t, y): converted_children = [ self.convert(child, t, y) for child in symbol.children ] + # Special functions if symbol.function == np.min: return casadi.mmin(*converted_children) elif symbol.function == np.max: return casadi.mmax(*converted_children) + elif not isinstance( + symbol.function, pybamm.GetCurrent + ) and symbol.function.__name__.startswith("elementwise_grad_of_"): + differentiating_child_idx = int(symbol.function.__name__[-1]) + # Create dummy symbolic variables in order to differentiate using CasADi + dummy_vars = [ + casadi.SX.sym("y_" + str(i)) for i in range(len(converted_children)) + ] + func_diff = casadi.gradient( + symbol.differentiated_function(*dummy_vars), + dummy_vars[differentiating_child_idx], + ) + # Create function and evaluate it using the children + casadi_func_diff = casadi.Function("func_diff", dummy_vars, [func_diff]) + return casadi_func_diff(*converted_children) + # Other functions else: return symbol._function_evaluate(converted_children) - elif isinstance(symbol, pybamm.Concatenation): converted_children = [ self.convert(child, t, y) for child in symbol.children diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index 8d4ce0302d..c8a9549127 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -120,7 +120,6 @@ def default_solver(self): # Different solver depending on whether we solve ODEs or DAEs dimensionality = self.options["dimensionality"] if dimensionality == 0: - return pybamm.CasadiSolver() - # return pybamm.ScipySolver() + return pybamm.ScipySolver() else: return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py index 7812f87856..e5e6167592 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spme.py +++ b/pybamm/models/full_battery_models/lithium_ion/spme.py @@ -124,7 +124,6 @@ def default_solver(self): # Different solver depending on whether we solve ODEs or DAEs dimensionality = self.options["dimensionality"] if dimensionality == 0: - return pybamm.CasadiSolver() - # return pybamm.ScipySolver() + return pybamm.ScipySolver() else: return pybamm.ScikitsDaeSolver() diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index d4b828ead5..2840474775 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -88,6 +88,7 @@ def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): "reltol": self.rtol, "abstol": self.atol, "output_t0": True, + "disable_internal_warnings": True, } if self.method == "idas": options["calc_ic"] = True @@ -95,17 +96,22 @@ def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): # set up and solve integrator = casadi.integrator("F", self.method, problem, options) try: - # return solution - sol = integrator(x0=y0) + # Try solving + len_rhs = problem["x"].size()[0] + y0_diff, y0_alg = np.split(y0, [len_rhs]) + sol = integrator(x0=y0_diff, z0=y0_alg) y_values = np.concatenate([sol["xf"].full(), sol["zf"].full()]) return pybamm.Solution(t_eval, y_values, None, None, "final time") except RuntimeError as e: + # If it doesn't work raise error raise pybamm.SolverError(e.args[0]) def set_up(self, model): - "Skip classic set up with this solver" - pass + "Skip classic set up with this solver, just reset initial conditions" + self.y0 = model.concatenated_initial_conditions - # def calculate_consistent_initial_conditions(self): - # "No need to calculate initial conditions separately with this solver" - # pass + def calculate_consistent_initial_conditions( + self, rhs, algebraic, y0_guess, jac=None + ): + "No need to calculate initial conditions separately with this solver" + return y0_guess diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index efd5431df2..5b60d048f3 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -223,7 +223,12 @@ def set_up_casadi(self, model): """ # Convert model attributes to casadi t_casadi = casadi.SX.sym("t") - y_casadi = casadi.SX.sym("y", len(model.concatenated_initial_conditions)) + y0 = model.concatenated_initial_conditions + y_diff = casadi.SX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) + y_alg = casadi.SX.sym( + "y_alg", len(model.concatenated_algebraic.evaluate(0, y0)) + ) + y_casadi = casadi.vertcat(y_diff, y_alg) pybamm.logger.info("Converting RHS to CasADi") concatenated_rhs = model.concatenated_rhs.to_casadi(t_casadi, y_casadi) pybamm.logger.info("Converting algebraic to CasADi") @@ -248,6 +253,7 @@ def set_up_casadi(self, model): if model.use_jacobian: + pybamm.logger.info("Calculating jacobian") casadi_jac = casadi.jacobian(all_states, y_casadi) casadi_jac_fn = casadi.Function( "jacobian", [t_casadi, y_casadi], [casadi_jac] @@ -315,7 +321,12 @@ def eval_event(t, y): self.jacobian = jacobian # Create CasADi problem for the CasADi solver - self.casadi_problem = {"x": y_casadi, "ode": concatenated_rhs} + self.casadi_problem = { + "x": y_diff, + "z": y_alg, + "ode": concatenated_rhs, + "alg": concatenated_algebraic, + } def calculate_consistent_initial_conditions( self, rhs, algebraic, y0_guess, jac=None diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index 386c6b4c7a..ecd9965f87 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -204,6 +204,7 @@ def eval_event(t, y): # Create function to evaluate jacobian if model.use_jacobian: + pybamm.logger.info("Calculating jacobian") casadi_jac = casadi.jacobian(concatenated_rhs, y_casadi) casadi_jac_fn = casadi.Function( "jacobian", [t_casadi, y_casadi], [casadi_jac] diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index bb10dd2429..28d1586b36 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -21,6 +21,10 @@ def test_multi_var_function(arg1, arg2): return arg1 + arg2 +def test_multi_var_function_cube(arg1, arg2): + return arg1 + arg2 ** 3 + + class TestFunction(unittest.TestCase): def test_constant_functions(self): d = pybamm.Scalar(6) @@ -52,8 +56,9 @@ def test_function_of_one_variable(self): logvar.evaluate(y=y, known_evals={})[0], np.log1p(y) ) - def test_with_autograd(self): + def test_diff(self): a = pybamm.StateVector(slice(0, 1)) + b = pybamm.StateVector(slice(1, 2)) y = np.array([5]) func = pybamm.Function(test_function, a) self.assertEqual(func.diff(a).evaluate(y=y), 2) @@ -68,6 +73,21 @@ def test_with_autograd(self): # multiple variables func = pybamm.Function(test_multi_var_function, 4 * a, 3 * a) self.assertEqual(func.diff(a).evaluate(y=y), 7) + func = pybamm.Function(test_multi_var_function, 4 * a, 3 * b) + self.assertEqual(func.diff(a).evaluate(y=np.array([5, 6])), 4) + self.assertEqual(func.diff(b).evaluate(y=np.array([5, 6])), 3) + func = pybamm.Function(test_multi_var_function_cube, 4 * a, 3 * b) + self.assertEqual(func.diff(a).evaluate(y=np.array([5, 6])), 4) + self.assertEqual( + func.diff(b).evaluate(y=np.array([5, 6])), 3 * 3 * (3 * 6) ** 2 + ) + + # exceptions + func = pybamm.Function( + test_multi_var_function_cube, 4 * a, 3 * b, derivative="derivative" + ) + with self.assertRaises(ValueError): + func.diff(a) def test_function_of_multiple_variables(self): a = pybamm.Variable("a") diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 0eb7e34762..d21966eb94 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -4,6 +4,7 @@ import casadi import math import numpy as np +import autograd.numpy as anp import pybamm import unittest from tests import get_mesh_for_testing, get_1p1d_discretisation_for_testing @@ -29,13 +30,13 @@ def sin(x): return np.sin(x) f = pybamm.Function(sin, b) - self.assertEqual((f).to_casadi(), casadi.SX(np.sin(1))) + self.assertEqual(f.to_casadi(), casadi.SX(np.sin(1))) def myfunction(x, y): return x + y f = pybamm.Function(myfunction, b, d) - self.assertEqual((f).to_casadi(), casadi.SX(3)) + self.assertEqual(f.to_casadi(), casadi.SX(3)) # addition self.assertEqual((a + b).to_casadi(), casadi.SX(1)) @@ -107,6 +108,25 @@ def test_concatenations(self): y_eval = np.linspace(0, 1, expr.size) self.assertTrue(casadi.is_equal(f(y_eval), casadi.SX(expr.evaluate(y=y_eval)))) + def test_convert_differentiated_function(self): + a = pybamm.Scalar(0) + b = pybamm.Scalar(1) + + # function + def sin(x): + return anp.sin(x) + + f = pybamm.Function(sin, b).diff(b) + self.assertEqual(f.to_casadi(), casadi.SX(np.cos(1))) + + def myfunction(x, y): + return x + y ** 3 + + f = pybamm.Function(myfunction, a, b).diff(a) + self.assertEqual(f.to_casadi(), casadi.SX(1)) + f = pybamm.Function(myfunction, a, b).diff(b) + self.assertEqual(f.to_casadi(), casadi.SX(3)) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index cfed566600..b2c75802ed 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -80,43 +80,41 @@ def test_model_solver(self): solution.total_time, solution.solve_time + solution.set_up_time ) - # - # def test_model_step(self): - # # Create model - # model = pybamm.BaseModel() - # domain = ["negative electrode", "separator", "positive electrode"] - # var = pybamm.Variable("var", domain=domain) - # model.rhs = {var: 0.1 * var} - # model.initial_conditions = {var: 1} - # # No need to set parameters; can use base discretisation (no spatial operators) - # - # # create discretisation - # mesh = get_mesh_for_testing() - # spatial_methods = {"macroscale": pybamm.FiniteVolume} - # disc = pybamm.Discretisation(mesh, spatial_methods) - # disc.process_model(model) - # - # solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") - # - # # Step once - # dt = 0.1 - # step_sol = solver.step(model, dt) - # np.testing.assert_array_equal(step_sol.t, [0, dt]) - # np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) - # - # # Step again (return 5 points) - # step_sol_2 = solver.step(model, dt, npts=5) - # np.testing.assert_array_equal(step_sol_2.t, np.linspace(dt, 2 * dt, 5)) - # np.testing.assert_allclose(step_sol_2.y[0], np.exp(0.1 * step_sol_2.t)) - # - # # append solutions - # step_sol.append(step_sol_2) - # - # # Check steps give same solution as solve - # t_eval = step_sol.t - # solution = solver.solve(model, t_eval) - # np.testing.assert_allclose(solution.y[0], step_sol.y[0]) - # + def test_model_step(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + + # Step once + dt = 0.1 + step_sol = solver.step(model, dt) + np.testing.assert_array_equal(step_sol.t, [0, dt]) + np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # Step again (return 5 points) + step_sol_2 = solver.step(model, dt, npts=5) + np.testing.assert_array_equal(step_sol_2.t, np.linspace(dt, 2 * dt, 5)) + np.testing.assert_allclose(step_sol_2.y[0], np.exp(0.1 * step_sol_2.t)) + + # append solutions + step_sol.append(step_sol_2) + + # Check steps give same solution as solve + t_eval = step_sol.t + solution = solver.solve(model, t_eval) + np.testing.assert_allclose(solution.y[0], step_sol.y[0]) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 58ad303efb..e0bed5251f 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -269,6 +269,9 @@ def test_model_step(self): t_eval = step_sol.t solution = solver.solve(model, t_eval) np.testing.assert_allclose(solution.y[0], step_sol.y[0]) + import ipdb + + ipdb.set_trace() def test_model_solver_with_event_with_casadi(self): # Create model From df58218e5a6615bdfc9034e632f8d2ab67027715 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 22 Oct 2019 23:15:00 -0400 Subject: [PATCH 041/122] #664 testing lead acid example --- examples/scripts/compare_lead_acid.py | 13 +++++++------ pybamm/expression_tree/functions.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/scripts/compare_lead_acid.py b/examples/scripts/compare_lead_acid.py index a10669af3f..028a2a1629 100644 --- a/examples/scripts/compare_lead_acid.py +++ b/examples/scripts/compare_lead_acid.py @@ -41,17 +41,18 @@ # solve model solutions = [None] * len(models) -t_eval = np.linspace(0, 0.66, 1000) +t_eval = np.linspace(0, 1, 1000) for i, model in enumerate(models): - model.convert_to_format = "casadi" - solution = pybamm.CasadiSolver().solve(model, t_eval) + solution = model.default_solver.solve(model, t_eval) solutions[i] = solution # plot output_variables = [ - "Electrolyte pressure", - "Electrolyte concentration", - "Volume-averaged velocity [m.s-1]", + "Interfacial current density [A.m-2]", + "Electrolyte concentration [mol.m-3]", + "Current [A]", + "Porosity", + "Electrolyte potential [V]", "Terminal voltage [V]", ] plot = pybamm.QuickPlot(models, mesh, solutions, output_variables) diff --git a/pybamm/expression_tree/functions.py b/pybamm/expression_tree/functions.py index 3bba9f8f6d..650d4a6767 100644 --- a/pybamm/expression_tree/functions.py +++ b/pybamm/expression_tree/functions.py @@ -139,9 +139,9 @@ def _jac(self, variable): # calculate the required partial jacobians and add them jacobian = None children = self.orphans - for child in children: + for i, child in enumerate(children): if not child.evaluates_to_number(): - jac_fun = self._diff(children) * child.jac(variable) + jac_fun = self._diff(children, i) * child.jac(variable) jac_fun.domain = self.domain if jacobian is None: From 51b577828c0775b1457ba6ab1464526350afd755 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 22 Oct 2019 23:28:28 -0400 Subject: [PATCH 042/122] #664 testing 3d lead-acid --- examples/scripts/compare_lead_acid_3D.py | 3 +-- pybamm/solvers/base_solver.py | 6 ++++-- pybamm/solvers/casadi_solver.py | 11 ----------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/examples/scripts/compare_lead_acid_3D.py b/examples/scripts/compare_lead_acid_3D.py index 5fa825328d..8d441092ac 100644 --- a/examples/scripts/compare_lead_acid_3D.py +++ b/examples/scripts/compare_lead_acid_3D.py @@ -21,8 +21,7 @@ # {"current collector": "potential pair", "dimensionality": 2}, name="2+1D LOQS" # ), pybamm.lead_acid.Full( - {"current collector": "potential pair", "dimensionality": 1}, - name="1+1D Full", + {"current collector": "potential pair", "dimensionality": 1}, name="1+1D Full" ), # pybamm.lead_acid.Full( # {"dimensionality": 1}, name="1+1D uniform Full" diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 6674544bd8..e0a4758ea9 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -74,7 +74,7 @@ def solve(self, model, t_eval): # Set up timer = pybamm.Timer() start_time = timer.time() - if model.convert_to_format == "casadi": + if model.convert_to_format == "casadi" or isinstance(self, pybamm.CasadiSolver): self.set_up_casadi(model) else: self.set_up(model) @@ -131,7 +131,9 @@ def step(self, model, dt, npts=2): # Run set up on first step if not hasattr(self, "y0"): start_time = timer.time() - if model.convert_to_format == "casadi": + if model.convert_to_format == "casadi" or isinstance( + self, pybamm.CasadiSolver + ): self.set_up_casadi(model) else: self.set_up(model) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 2840474775..42080444fd 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -44,13 +44,6 @@ def compute_solution(self, model, t_eval): The times at which to compute the solution """ - # Convert to CasADi if not already done - if not hasattr(self, "casadi_problem"): - pybamm.logger.info( - "Converting model to CasADi format, required for CasADi solver" - ) - self.set_up_casadi(model) - timer = pybamm.Timer() solve_start_time = timer.time() @@ -106,10 +99,6 @@ def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): # If it doesn't work raise error raise pybamm.SolverError(e.args[0]) - def set_up(self, model): - "Skip classic set up with this solver, just reset initial conditions" - self.y0 = model.concatenated_initial_conditions - def calculate_consistent_initial_conditions( self, rhs, algebraic, y0_guess, jac=None ): From 9df5a90e36fcbdceeec93e98cfddc453f4c91da1 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Wed, 23 Oct 2019 08:39:28 -0400 Subject: [PATCH 043/122] #664 fix tests and style --- pybamm/solvers/base_solver.py | 1 - .../test_operations/test_convert_to_casadi.py | 1 - tests/unit/test_solvers/test_scipy_solver.py | 3 --- 3 files changed, 5 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index e0a4758ea9..3b95b10f73 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -1,7 +1,6 @@ # # Base solver class # -import casadi import pybamm import numpy as np diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index d21966eb94..01de9b159d 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -2,7 +2,6 @@ # Test for the Simplify class # import casadi -import math import numpy as np import autograd.numpy as anp import pybamm diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index e0bed5251f..58ad303efb 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -269,9 +269,6 @@ def test_model_step(self): t_eval = step_sol.t solution = solver.solve(model, t_eval) np.testing.assert_allclose(solution.y[0], step_sol.y[0]) - import ipdb - - ipdb.set_trace() def test_model_solver_with_event_with_casadi(self): # Create model From 28eed5537877a2e79b93e3d165264f2f3389340a Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 24 Oct 2019 17:22:21 -0400 Subject: [PATCH 044/122] #664 pass extra options to casadi --- pybamm/solvers/casadi_solver.py | 4 +++- tests/unit/test_solvers/test_casadi_solver.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 42080444fd..5f373246f5 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -27,8 +27,10 @@ def __init__( root_method="lm", root_tol=1e-6, max_steps=1000, + **extra_options, ): super().__init__(method, rtol, atol, root_method, root_tol, max_steps) + self.extra_options = extra_options def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. In this class, we @@ -81,8 +83,8 @@ def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): "reltol": self.rtol, "abstol": self.atol, "output_t0": True, - "disable_internal_warnings": True, } + options.update(self.extra_options) if self.method == "idas": options["calc_ic"] = True diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index b2c75802ed..6d6b45eecd 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -45,7 +45,13 @@ def test_integrate_failure(self): y0 = np.array([1]) t_eval = np.linspace(0, 3, 100) - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + solver = pybamm.CasadiSolver( + rtol=1e-8, + atol=1e-8, + method="idas", + disable_internal_warnings=True, + regularity_check=False, + ) problem = {"x": y, "ode": sqrt_decay} # Expect solver to fail when y goes negative with self.assertRaises(pybamm.SolverError): From 0a7d5f781c65dae23999d34977f18a07d7476715 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 24 Oct 2019 22:52:19 -0400 Subject: [PATCH 045/122] #664 fix docs --- docs/source/expression_tree/index.rst | 6 ------ pybamm/models/base_model.py | 5 +++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/source/expression_tree/index.rst b/docs/source/expression_tree/index.rst index 7049d4a275..3eedd7b622 100644 --- a/docs/source/expression_tree/index.rst +++ b/docs/source/expression_tree/index.rst @@ -17,10 +17,4 @@ Expression Tree broadcasts functions interpolant -<<<<<<< HEAD operations/index -======= - evaluate - simplify - jacobian ->>>>>>> master diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index aaf8474eb6..9d192af47a 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -73,12 +73,13 @@ class BaseModel(object): algebraic equations, Jacobain (if using) and events into a different format: - None: keep PyBaMM expression tree structure. - - "python": convert into pure python code that will calculate the result of + - "python": convert into pure python code that will calculate the result of \ calling `evaluate(t, y)` on the given expression treeself. - - "casadi": convert into CasADi expression tree, which then uses CasADi's + - "casadi": convert into CasADi expression tree, which then uses CasADi's \ algorithm to calculate the Jacobian. Default is "python". + """ def __init__(self, name="Unnamed model"): From 5143e07f9c82e9e40fb680bd1e6b807327c5a5e2 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Fri, 25 Oct 2019 09:04:16 +0100 Subject: [PATCH 046/122] #678 fix dimension typo --- .../submodels/electrolyte/base_electrolyte_conductivity.py | 2 +- pybamm/parameters/standard_parameters_lead_acid.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py index 7d685dc7fe..cb3c49e64f 100644 --- a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py +++ b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py @@ -250,7 +250,7 @@ def _get_domain_current_variables(self, i_e, domain=None): variables = { domain + " electrolyte current density": i_e, - domain + " electrolyte current density [V]": i_e * i_typ, + domain + " electrolyte current density [A.m-2]": i_e * i_typ, } return variables diff --git a/pybamm/parameters/standard_parameters_lead_acid.py b/pybamm/parameters/standard_parameters_lead_acid.py index 90840bd316..496d5fb85e 100644 --- a/pybamm/parameters/standard_parameters_lead_acid.py +++ b/pybamm/parameters/standard_parameters_lead_acid.py @@ -416,6 +416,9 @@ def U_p_dimensional(c_e, T): c_n_init = c_e_init c_p_init = c_e_init +# Thermal effects not implemented for lead-acid, but parameter needed for consistency +Theta = pybamm.Scalar(0) # ratio of typical temperature change to ambient temperature + # -------------------------------------------------------------------------------------- "5. Dimensionless Functions" From d1be2b9afaa491e1d4fc829130eb684b31bdfbc7 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Fri, 25 Oct 2019 17:23:09 -0400 Subject: [PATCH 047/122] #664 now getting ida conv fail :( --- examples/scripts/compare-dae-solver.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/scripts/compare-dae-solver.py b/examples/scripts/compare-dae-solver.py index 05476790cf..dd9c15e7da 100644 --- a/examples/scripts/compare-dae-solver.py +++ b/examples/scripts/compare-dae-solver.py @@ -17,7 +17,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 60, var.x_s: 100, var.x_p: 60, var.r_n: 50, var.r_p: 50} +var_pts = {var.x_n: 50, var.x_s: 10, var.x_p: 50, var.r_n: 20, var.r_p: 20} # var_pts = model.default_var_pts mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -26,13 +26,14 @@ disc.process_model(model) # solve model -t_eval = np.linspace(0, 0.2, 100) +t_eval = np.linspace(0, 0.17, 100) +casadi_sol = pybamm.CasadiSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) klu_sol = pybamm.IDAKLU(atol=1e-8, rtol=1e-8).solve(model, t_eval) scikits_sol = pybamm.ScikitsDaeSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) # plot -models = [model, model] -solutions = [scikits_sol, klu_sol] +models = [model, model, model] +solutions = [scikits_sol, klu_sol, casadi_sol] plot = pybamm.QuickPlot(models, mesh, solutions) plot.dynamic_plot() From 372fa0cfb1e93658ec7651622ba84cd55d03a69b Mon Sep 17 00:00:00 2001 From: Travis CI Date: Fri, 25 Oct 2019 21:46:03 -0400 Subject: [PATCH 048/122] #664 fix casadi solver --- examples/scripts/compare-dae-solver.py | 4 +--- examples/scripts/compare_lithium_ion.py | 2 +- pybamm/solvers/casadi_solver.py | 5 ----- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/scripts/compare-dae-solver.py b/examples/scripts/compare-dae-solver.py index dd9c15e7da..99ee230333 100644 --- a/examples/scripts/compare-dae-solver.py +++ b/examples/scripts/compare-dae-solver.py @@ -16,9 +16,7 @@ # set mesh var = pybamm.standard_spatial_vars - -var_pts = {var.x_n: 50, var.x_s: 10, var.x_p: 50, var.r_n: 20, var.r_p: 20} -# var_pts = model.default_var_pts +var_pts = {var.x_n: 50, var.x_s: 50, var.x_p: 50, var.r_n: 20, var.r_p: 20} mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index fc06298566..2d37f5ced3 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -48,7 +48,7 @@ t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): model.convert_to_format = "casadi" - solutions[i] = model.default_solver.solve(model, t_eval) + solutions[i] = pybamm.CasadiSolver().solve(model, t_eval) # plot plot = pybamm.QuickPlot(models, mesh, solutions) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 5f373246f5..b12d36f17d 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -101,8 +101,3 @@ def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): # If it doesn't work raise error raise pybamm.SolverError(e.args[0]) - def calculate_consistent_initial_conditions( - self, rhs, algebraic, y0_guess, jac=None - ): - "No need to calculate initial conditions separately with this solver" - return y0_guess From 933354fc529e3371f47d993941124c48ff6fd37e Mon Sep 17 00:00:00 2001 From: Travis CI Date: Fri, 25 Oct 2019 21:47:02 -0400 Subject: [PATCH 049/122] #664 rename IDAKLU to IDAKLUSolver --- examples/scripts/compare-dae-solver.py | 2 +- pybamm/__init__.py | 2 +- pybamm/solvers/idaklu_solver.py | 2 +- results/2plus1D/dfn_2plus1D.py | 2 +- results/2plus1D/spm_2plus1D.py | 2 +- results/2plus1D/spme_2plus1D.py | 2 +- tests/integration/test_solvers/test_idaklu.py | 2 +- tests/unit/test_solvers/test_idaklu_solver.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/scripts/compare-dae-solver.py b/examples/scripts/compare-dae-solver.py index 99ee230333..e4dff68a3d 100644 --- a/examples/scripts/compare-dae-solver.py +++ b/examples/scripts/compare-dae-solver.py @@ -27,7 +27,7 @@ t_eval = np.linspace(0, 0.17, 100) casadi_sol = pybamm.CasadiSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) -klu_sol = pybamm.IDAKLU(atol=1e-8, rtol=1e-8).solve(model, t_eval) +klu_sol = pybamm.IDAKLUSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) scikits_sol = pybamm.ScikitsDaeSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) # plot diff --git a/pybamm/__init__.py b/pybamm/__init__.py index bb13078333..3a94c5dc99 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -266,7 +266,7 @@ def version(formatted=False): from .solvers.scikits_dae_solver import ScikitsDaeSolver from .solvers.scikits_ode_solver import ScikitsOdeSolver, have_scikits_odes from .solvers.scipy_solver import ScipySolver -from .solvers.idaklu_solver import IDAKLU, have_idaklu +from .solvers.idaklu_solver import IDAKLUSolver, have_idaklu # diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index b055c95db5..131f0fdd8b 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -17,7 +17,7 @@ def have_idaklu(): return idaklu_spec is None -class IDAKLU(pybamm.DaeSolver): +class IDAKLUSolver(pybamm.DaeSolver): """Solve a discretised model, using sundials with the KLU sparse linear solver. Parameters diff --git a/results/2plus1D/dfn_2plus1D.py b/results/2plus1D/dfn_2plus1D.py index 9beff8d5a4..86cbad603c 100644 --- a/results/2plus1D/dfn_2plus1D.py +++ b/results/2plus1D/dfn_2plus1D.py @@ -46,7 +46,7 @@ tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) t_eval = np.linspace(0, t_end, 120) -solution = pybamm.IDAKLU().solve(model, t_eval) +solution = pybamm.IDAKLUSolver().solve(model, t_eval) # TO DO: 2+1D automated plotting phi_s_cn = pybamm.ProcessedVariable( diff --git a/results/2plus1D/spm_2plus1D.py b/results/2plus1D/spm_2plus1D.py index 1b9a08411c..943a1afdd5 100644 --- a/results/2plus1D/spm_2plus1D.py +++ b/results/2plus1D/spm_2plus1D.py @@ -46,7 +46,7 @@ tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) t_eval = np.linspace(0, t_end, 120) -solution = pybamm.IDAKLU().solve(model, t_eval) +solution = pybamm.IDAKLUSolver().solve(model, t_eval) # TO DO: 2+1D automated plotting phi_s_cn = pybamm.ProcessedVariable( diff --git a/results/2plus1D/spme_2plus1D.py b/results/2plus1D/spme_2plus1D.py index d356f1a62f..1cef748871 100644 --- a/results/2plus1D/spme_2plus1D.py +++ b/results/2plus1D/spme_2plus1D.py @@ -46,7 +46,7 @@ tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) t_eval = np.linspace(0, t_end, 120) -solution = pybamm.IDAKLU().solve(model, t_eval) +solution = pybamm.IDAKLUSolver().solve(model, t_eval) # TO DO: 2+1D automated plotting phi_s_cn = pybamm.ProcessedVariable( diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 7885d54433..ea78201f74 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -16,7 +16,7 @@ def test_on_spme(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) t_eval = np.linspace(0, 0.2, 100) - solution = pybamm.IDAKLU().solve(model, t_eval) + solution = pybamm.IDAKLUSolver().solve(model, t_eval) np.testing.assert_array_less(1, solution.t.size) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index a644ec7e3b..6d442e12d0 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -53,7 +53,7 @@ def rhs(t, y): def alg(t, y): return np.array([1 - y[1]]) - solver = pybamm.IDAKLU() + solver = pybamm.IDAKLUSolver() solver.residuals = res solver.rhs = rhs solver.algebraic = alg From b1626991e345279bdec46c8ebb3e7e38dd61f372 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Fri, 25 Oct 2019 22:06:07 -0400 Subject: [PATCH 050/122] #664 codacy --- pybamm/expression_tree/functions.py | 39 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pybamm/expression_tree/functions.py b/pybamm/expression_tree/functions.py index 0464bb02ab..1f1d28e524 100644 --- a/pybamm/expression_tree/functions.py +++ b/pybamm/expression_tree/functions.py @@ -95,9 +95,9 @@ def diff(self, variable): # if variable appears in the function,use autograd to differentiate # function, and apply chain rule if variable.id in [symbol.id for symbol in child.pre_order()]: - partial_derivatives[i] = self._diff(children, i) * child.diff( - variable - ) + partial_derivatives[i] = self._function_diff( + children, i + ) * child.diff(variable) # remove None entries partial_derivatives = list(filter(None, partial_derivatives)) @@ -108,8 +108,11 @@ def diff(self, variable): return derivative - def _diff(self, children, idx): - """ See :meth:`pybamm.Symbol._diff()`. """ + def _function_diff(self, children, idx): + """ + Derivative with respect to child number 'idx'. + See :meth:`pybamm.Symbol._diff()`. + """ # Store differentiated function, needed in case we want to convert to CasADi if self.derivative == "autograd": return Function( @@ -146,7 +149,7 @@ def _function_jac(self, children_jacs): children = self.orphans for i, child in enumerate(children): if not child.evaluates_to_number(): - jac_fun = self._diff(children, i) * children_jacs[i] + jac_fun = self._function_diff(children, i) * children_jacs[i] jac_fun.domain = [] if jacobian is None: jacobian = jac_fun @@ -272,8 +275,8 @@ class Cos(SpecificFunction): def __init__(self, child): super().__init__(np.cos, child) - def _diff(self, children, idx): - """ See :meth:`pybamm.Symbol._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Symbol._function_diff()`. """ return -Sin(children[0]) @@ -288,8 +291,8 @@ class Cosh(SpecificFunction): def __init__(self, child): super().__init__(np.cosh, child) - def _diff(self, children, idx): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Sinh(children[0]) @@ -304,8 +307,8 @@ class Exponential(SpecificFunction): def __init__(self, child): super().__init__(np.exp, child) - def _diff(self, children, idx): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Exponential(children[0]) @@ -320,8 +323,8 @@ class Log(SpecificFunction): def __init__(self, child): super().__init__(np.log, child) - def _diff(self, children, idx): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return 1 / children[0] @@ -346,8 +349,8 @@ class Sin(SpecificFunction): def __init__(self, child): super().__init__(np.sin, child) - def _diff(self, children, idx): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Cos(children[0]) @@ -362,8 +365,8 @@ class Sinh(SpecificFunction): def __init__(self, child): super().__init__(np.sinh, child) - def _diff(self, children, idx): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Cosh(children[0]) From b46512962a9991e5b5031e0174d5b1c33808dc0c Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Sat, 26 Oct 2019 21:25:12 -0400 Subject: [PATCH 051/122] #664 coverage --- pybamm/parameters/electrical_parameters.py | 15 +----- pybamm/solvers/ode_solver.py | 1 - .../test_operations/test_convert_to_casadi.py | 11 +++++ .../test_electrical_parameters.py | 1 + tests/unit/test_solvers/test_ode_solver.py | 4 ++ .../unit/test_solvers/test_scikits_solvers.py | 46 ++++++++++--------- tests/unit/test_solvers/test_scipy_solver.py | 43 +++++++++-------- 7 files changed, 64 insertions(+), 57 deletions(-) diff --git a/pybamm/parameters/electrical_parameters.py b/pybamm/parameters/electrical_parameters.py index f9260935c0..853823c55a 100644 --- a/pybamm/parameters/electrical_parameters.py +++ b/pybamm/parameters/electrical_parameters.py @@ -1,21 +1,10 @@ # # Standard electrical parameters # -import casadi import pybamm import numpy as np -def abs_non_zero(x): - if x == 0: # pragma: no cover - return 1 - else: - if isinstance(x, casadi.SX): - return casadi.fabs(x) - else: - return abs(x) - - # -------------------------------------------------------------------------------------- # Dimensional Parameters I_typ = pybamm.Parameter("Typical current [A]") @@ -24,9 +13,7 @@ def abs_non_zero(x): n_electrodes_parallel = pybamm.Parameter( "Number of electrodes connected in parallel to make a cell" ) -i_typ = pybamm.Function( - abs_non_zero, (I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc)) -) +i_typ = I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc) voltage_low_cut_dimensional = pybamm.Parameter("Lower voltage cut-off [V]") voltage_high_cut_dimensional = pybamm.Parameter("Upper voltage cut-off [V]") diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index af416073ec..9b650241f0 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -208,7 +208,6 @@ def eval_event(t, y): # Create function to evaluate jacobian if model.use_jacobian: - pybamm.logger.info("Calculating jacobian") casadi_jac = casadi.jacobian(concatenated_rhs, y_casadi) casadi_jac_fn = casadi.Function( diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 01de9b159d..f861bb09fd 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -76,6 +76,7 @@ def test_special_functions(self): a = np.array([1, 2, 3, 4, 5]) pybamm_a = pybamm.Array(a) self.assertEqual(pybamm.min(pybamm_a).to_casadi(), casadi.SX(1)) + self.assertEqual(pybamm.max(pybamm_a).to_casadi(), casadi.SX(5)) def test_concatenations(self): y = np.linspace(0, 1, 10)[:, np.newaxis] @@ -126,6 +127,16 @@ def myfunction(x, y): f = pybamm.Function(myfunction, a, b).diff(b) self.assertEqual(f.to_casadi(), casadi.SX(3)) + def test_errors(self): + y = pybamm.StateVector(slice(0, 10)) + with self.assertRaisesRegex( + ValueError, "Must provide a 'y' for converting state vectors" + ): + y.to_casadi() + var = pybamm.Variable("var") + with self.assertRaisesRegex(TypeError, "Cannot convert symbol of type"): + var.to_casadi() + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_parameters/test_electrical_parameters.py b/tests/unit/test_parameters/test_electrical_parameters.py index 8f8f7cad79..fb5d97ad8e 100644 --- a/tests/unit/test_parameters/test_electrical_parameters.py +++ b/tests/unit/test_parameters/test_electrical_parameters.py @@ -1,6 +1,7 @@ # # Tests for the electrical parameters # +import casadi import pybamm import unittest diff --git a/tests/unit/test_solvers/test_ode_solver.py b/tests/unit/test_solvers/test_ode_solver.py index 009dbcca23..5469e4f726 100644 --- a/tests/unit/test_solvers/test_ode_solver.py +++ b/tests/unit/test_solvers/test_ode_solver.py @@ -24,6 +24,10 @@ def test_wrong_solver(self): pybamm.SolverError, "Cannot use ODE solver to solve model with DAEs" ): solver.solve(model, None) + with self.assertRaisesRegex( + pybamm.SolverError, "Cannot use ODE solver to solve model with DAEs" + ): + solver.set_up_casadi(model) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index a46e977925..c0977fd709 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -702,28 +702,30 @@ def test_model_solver_ode_events_casadi(self): def test_model_solver_dae_events_casadi(self): # Create model model = pybamm.BaseModel() - model.convert_to_format = "casadi" - whole_cell = ["negative electrode", "separator", "positive electrode"] - var1 = pybamm.Variable("var1", domain=whole_cell) - var2 = pybamm.Variable("var2", domain=whole_cell) - model.rhs = {var1: 0.1 * var1} - model.algebraic = {var2: 2 * var1 - var2} - model.initial_conditions = {var1: 1, var2: 2} - model.events = { - "var1 = 1.5": pybamm.min(var1 - 1.5), - "var2 = 2.5": pybamm.min(var2 - 2.5), - } - disc = get_discretisation_for_testing() - disc.process_model(model) - - # Solve - solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8) - t_eval = np.linspace(0, 5, 100) - solution = solver.solve(model, t_eval) - np.testing.assert_array_less(solution.y[0], 1.5) - np.testing.assert_array_less(solution.y[-1], 2.5) - np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + for use_jacobian in [True, False]: + model.use_jacobian = use_jacobian + model.convert_to_format = "casadi" + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = { + "var1 = 1.5": pybamm.min(var1 - 1.5), + "var2 = 2.5": pybamm.min(var2 - 2.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 3315db7199..127a751873 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -271,26 +271,29 @@ def test_model_step(self): def test_model_solver_with_event_with_casadi(self): # Create model model = pybamm.BaseModel() - model.convert_to_format = "casadi" - domain = ["negative electrode", "separator", "positive electrode"] - var = pybamm.Variable("var", domain=domain) - model.rhs = {var: -0.1 * var} - model.initial_conditions = {var: 1} - model.events = {"var=0.5": pybamm.min(var - 0.5)} - # No need to set parameters; can use base discretisation (no spatial operators) - - # create discretisation - mesh = get_mesh_for_testing() - spatial_methods = {"macroscale": pybamm.FiniteVolume} - disc = pybamm.Discretisation(mesh, spatial_methods) - disc.process_model(model) - # Solve - solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") - t_eval = np.linspace(0, 10, 100) - solution = solver.solve(model, t_eval) - self.assertLess(len(solution.t), len(t_eval)) - np.testing.assert_array_equal(solution.t, t_eval[: len(solution.t)]) - np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t)) + for use_jacobian in [True, False]: + model.use_jacobian = use_jacobian + model.convert_to_format = "casadi" + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -0.1 * var} + model.initial_conditions = {var: 1} + model.events = {"var=0.5": pybamm.min(var - 0.5)} + # No need to set parameters; can use base discretisation (no spatial + # operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval) + self.assertLess(len(solution.t), len(t_eval)) + np.testing.assert_array_equal(solution.t, t_eval[: len(solution.t)]) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t)) if __name__ == "__main__": From 9a0699f7a0dd927495f83f1f1874e1e8f02aa4da Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Sat, 26 Oct 2019 22:52:21 -0400 Subject: [PATCH 052/122] #664 implement abs --- .../expression_tree/operations/convert_to_casadi.py | 2 ++ pybamm/parameters/electrical_parameters.py | 4 +++- .../test_operations/test_convert_to_casadi.py | 11 +++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 395bb6ec78..d613badead 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -72,6 +72,8 @@ def _convert(self, symbol, t, y): return casadi.mmin(*converted_children) elif symbol.function == np.max: return casadi.mmax(*converted_children) + elif symbol.function == np.abs: + return casadi.fabs(*converted_children) elif not isinstance( symbol.function, pybamm.GetCurrent ) and symbol.function.__name__.startswith("elementwise_grad_of_"): diff --git a/pybamm/parameters/electrical_parameters.py b/pybamm/parameters/electrical_parameters.py index 853823c55a..8db66dbe29 100644 --- a/pybamm/parameters/electrical_parameters.py +++ b/pybamm/parameters/electrical_parameters.py @@ -13,7 +13,9 @@ n_electrodes_parallel = pybamm.Parameter( "Number of electrodes connected in parallel to make a cell" ) -i_typ = I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc) +i_typ = pybamm.Function( + np.abs, I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc) +) voltage_low_cut_dimensional = pybamm.Parameter("Lower voltage cut-off [V]") voltage_high_cut_dimensional = pybamm.Parameter("Upper voltage cut-off [V]") diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index f861bb09fd..a516cb7955 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -73,10 +73,13 @@ def test_convert_array_symbols(self): self.assertTrue(casadi.is_equal(outer.to_casadi(), casadi.SX(outer.evaluate()))) def test_special_functions(self): - a = np.array([1, 2, 3, 4, 5]) - pybamm_a = pybamm.Array(a) - self.assertEqual(pybamm.min(pybamm_a).to_casadi(), casadi.SX(1)) - self.assertEqual(pybamm.max(pybamm_a).to_casadi(), casadi.SX(5)) + a = pybamm.Array(np.array([1, 2, 3, 4, 5])) + self.assertEqual(pybamm.max(a).to_casadi(), casadi.SX(5)) + self.assertEqual(pybamm.min(a).to_casadi(), casadi.SX(1)) + b = pybamm.Array(np.array([-2])) + c = pybamm.Array(np.array([3])) + self.assertEqual(pybamm.Function(np.abs, b).to_casadi(), casadi.SX(2)) + self.assertEqual(pybamm.Function(np.abs, c).to_casadi(), casadi.SX(3)) def test_concatenations(self): y = np.linspace(0, 1, 10)[:, np.newaxis] From 91e051e8ce287824b41f238ae39f3208606228ff Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Sat, 26 Oct 2019 22:56:39 -0400 Subject: [PATCH 053/122] add extra windows instructions [ci skip] --- INSTALL-WINDOWS.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/INSTALL-WINDOWS.md b/INSTALL-WINDOWS.md index 3d2dc3cc8f..76a87100f1 100644 --- a/INSTALL-WINDOWS.md +++ b/INSTALL-WINDOWS.md @@ -21,7 +21,14 @@ typing $ sudo apt install git-core ``` -Now use git to clone the PyBaMM repository: +For easier integration with WSL, we recommend that you install PyBaMM in your *Windows* +Documents folder, for example by first navigating to + +```bash +$ cd /mnt/c/Users/USER_NAME/Documents +``` + +where USER_NAME is your username. Exact path to Windows documents may vary. Now use git to clone the PyBaMM repository: ```bash $ git clone https://github.com/pybamm-team/PyBaMM.git From b9d160cb25e5eefb40ef477f18162c4e8c270294 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Sun, 27 Oct 2019 09:14:21 -0400 Subject: [PATCH 054/122] #664 flake8 --- tests/unit/test_parameters/test_electrical_parameters.py | 1 - tests/unit/test_solvers/test_scipy_solver.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_parameters/test_electrical_parameters.py b/tests/unit/test_parameters/test_electrical_parameters.py index fb5d97ad8e..8f8f7cad79 100644 --- a/tests/unit/test_parameters/test_electrical_parameters.py +++ b/tests/unit/test_parameters/test_electrical_parameters.py @@ -1,7 +1,6 @@ # # Tests for the electrical parameters # -import casadi import pybamm import unittest diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 127a751873..f5d7e783ce 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -279,7 +279,7 @@ def test_model_solver_with_event_with_casadi(self): model.rhs = {var: -0.1 * var} model.initial_conditions = {var: 1} model.events = {"var=0.5": pybamm.min(var - 0.5)} - # No need to set parameters; can use base discretisation (no spatial + # No need to set parameters; can use base discretisation (no spatial # operators) # create discretisation From 2402649d82e70c80f51ee227c766cee3936da8a5 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Sun, 27 Oct 2019 09:16:12 -0400 Subject: [PATCH 055/122] #664 changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc2094ea7..80b9c20626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ## Features -- Add interface to CasADi solver -- Add option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians +- Add interface to CasADi solver ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) +- Add option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) - Add `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) - Add `Interpolant` class to interpolate experimental data (e.g. OCP curves) ([#661](https://github.com/pybamm-team/PyBaMM/pull/661)) @@ -19,7 +19,7 @@ ## Bug fixes -- Fix differentiation of functions that have more than one argument +- Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add warning if `ProcessedVariable` is called outisde its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) - Improve the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) From a0ad975bdc778c885b4a1698d7cde67874bb5221 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Sun, 27 Oct 2019 13:19:48 -0400 Subject: [PATCH 056/122] #664 restore examples --- examples/scripts/compare_lithium_ion.py | 5 ++--- examples/scripts/compare_lithium_ion_3D.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 2d37f5ced3..1aca4c7816 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -32,7 +32,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 50, var.x_s: 50, var.x_p: 50, var.r_n: 50, var.r_p: 50} +var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} # discretise models for model in models: @@ -47,8 +47,7 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): - model.convert_to_format = "casadi" - solutions[i] = pybamm.CasadiSolver().solve(model, t_eval) + solutions[i] = model.default_solver.solve(model, t_eval) # plot plot = pybamm.QuickPlot(models, mesh, solutions) diff --git a/examples/scripts/compare_lithium_ion_3D.py b/examples/scripts/compare_lithium_ion_3D.py index 9449f2e521..f271b63cf1 100644 --- a/examples/scripts/compare_lithium_ion_3D.py +++ b/examples/scripts/compare_lithium_ion_3D.py @@ -52,7 +52,6 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 1, 1000) for i, model in enumerate(models): - model.convert_to_format = "casadi" solution = model.default_solver.solve(model, t_eval) solutions[i] = solution From 466a4c4dee9ebf945b8d96198e3b1c04becd8d81 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Mon, 28 Oct 2019 08:10:45 -0400 Subject: [PATCH 057/122] #664 remove termination from casadi solver --- pybamm/solvers/casadi_solver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index b12d36f17d..373bbc0563 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -55,8 +55,8 @@ def compute_solution(self, model, t_eval): ) solve_time = timer.time() - solve_start_time - # Identify the event that caused termination - termination = self.get_termination_reason(solution, self.events) + # Events not implemented, termination is always 'final time' + termination = "final time" return solution, solve_time, termination From 5ed9dee4ba20f82c3bea3b071701f0d78b139fe4 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Mon, 28 Oct 2019 12:46:22 +0000 Subject: [PATCH 058/122] #688 created outline of sim class --- examples/scripts/run_simulation.py | 6 ++ pybamm/__init__.py | 3 +- pybamm/simulation.py | 99 ++++++++++++++++++++++++++++ tests/integration/test_simulation.py | 18 +++++ tests/unit/test_simulation.py | 12 ++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 examples/scripts/run_simulation.py create mode 100644 pybamm/simulation.py create mode 100644 tests/integration/test_simulation.py create mode 100644 tests/unit/test_simulation.py diff --git a/examples/scripts/run_simulation.py b/examples/scripts/run_simulation.py new file mode 100644 index 0000000000..ecb56d656d --- /dev/null +++ b/examples/scripts/run_simulation.py @@ -0,0 +1,6 @@ +import pybamm + +model = pybamm.lithium_ion.SPM() +sim = pybamm.Simulation(model) +sim.solve() +sim.plot() diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 482d6c9476..6ec3e0ccaf 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -265,7 +265,6 @@ def version(formatted=False): from .solvers.algebraic_solver import AlgebraicSolver from .solvers.idaklu_solver import IDAKLU, have_idaklu - # # Current profiles # @@ -282,6 +281,8 @@ def version(formatted=False): from .processed_variable import post_process_variables, ProcessedVariable from .quick_plot import QuickPlot, ax_min, ax_max +from .simulation import Simulation + # # Remove any imported modules, so we don't expose them as part of pybamm # diff --git a/pybamm/simulation.py b/pybamm/simulation.py new file mode 100644 index 0000000000..a995bca026 --- /dev/null +++ b/pybamm/simulation.py @@ -0,0 +1,99 @@ +import pybamm +import numpy as np + + +class Simulation: + def __init__(self, model): + + self._model = model + self._geometry = model.default_geometry + self._parameter_values = model.default_parameter_values + self._submesh_types = model.default_submesh_types + self._var_pts = model.default_var_pts + self._spatial_methods = model.default_spatial_methods + self._solver = model.default_solver + self._quick_plot_vars = None + + @property + def model(self): + return self._model + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, geometry): + self._geometry = geometry + + @property + def parameter_values(self): + return self.parameter_values + + @parameter_values.setter + def parameter_values(self, parameter_values): + self._parameter_values = parameter_values + + @property + def submesh_types(self): + return self._submesh_types + + @submesh_types.setter + def submesh_types(self, submesh_types): + # check of correct form + self._submesh_types = submesh_types + + @property + def var_pts(self): + return self._var_pts + + @var_pts.setter + def var_pts(self, var_pts): + self._var_pts = var_pts + + @property + def spatial_methods(self): + return self._spatial_methods + + @spatial_methods.setter + def spatial_methods(self, spatial_methods): + self._spatial_methods = spatial_methods + + @property + def solver(self): + return self._solver + + @solver.setter + def solver(self, solver): + self._solver = solver + + @property + def quick_plot_vars(self): + return self._quick_plot_vars + + @quick_plot_vars.setter + def quick_plot_vars(self, quick_plot_vars): + self._quick_plot_vars = quick_plot_vars + + @property + def solution(self): + return self._solution + + def solve(self, t_eval=None): + self._parameter_values.process_model(self._model) + self._parameter_values.process_geometry(self._geometry) + self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) + self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) + self._disc.process_model(self._model) + + if t_eval is None: + t_eval = np.linspace(0, 1, 100) + + self._solution = self.solver.solve(self._model, t_eval) + + def plot(self): + plot = pybamm.QuickPlot( + self._model, self._mesh, self._solution, self._quick_plot_vars + ) + plot.dynamic_plot() + diff --git a/tests/integration/test_simulation.py b/tests/integration/test_simulation.py new file mode 100644 index 0000000000..a3632f21bb --- /dev/null +++ b/tests/integration/test_simulation.py @@ -0,0 +1,18 @@ +import pybamm +import unittest + + +class TestSimulation(unittest.TestCase): + def test_run_with_spm(self): + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model) + sim.solve() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + unittest.main() diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py new file mode 100644 index 0000000000..605913560d --- /dev/null +++ b/tests/unit/test_simulation.py @@ -0,0 +1,12 @@ +import pybamm +import unittest + + +class TestSimulation(unittest.TestCase): + def test_set_model(self): + + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model) + + self.assertEqual(sim.model, model) + From 84206d59a13fc0b56ef051aa3f7e7e7e029a11e6 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 28 Oct 2019 14:06:28 +0000 Subject: [PATCH 059/122] #678 thermal comparison --- .../compare_comsol_thermal.py | 126 ++++++++++++++++ results/comsol_comparison/load_comsol_data.py | 138 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 results/comsol_comparison/compare_comsol_thermal.py create mode 100644 results/comsol_comparison/load_comsol_data.py diff --git a/results/comsol_comparison/compare_comsol_thermal.py b/results/comsol_comparison/compare_comsol_thermal.py new file mode 100644 index 0000000000..7c8d5d49a2 --- /dev/null +++ b/results/comsol_comparison/compare_comsol_thermal.py @@ -0,0 +1,126 @@ +import pybamm +import numpy as np +import os +import pickle +import scipy.interpolate as interp +import matplotlib.pyplot as plt + +# change working directory to the root of pybamm +os.chdir(pybamm.root_dir()) + +"-----------------------------------------------------------------------------" +"Load comsol data" + +comsol_variables = pickle.load( + open("input/comsol_results/comsol_thermal_1C.pickle", "rb") +) + +"-----------------------------------------------------------------------------" +"Create and solve pybamm model" + +# load model and geometry +pybamm.set_logging_level("INFO") +options = {"thermal": "x-full"} +pybamm_model = pybamm.lithium_ion.DFN(options) +geometry = pybamm_model.default_geometry + +# load parameters and process model and geometry +param = pybamm_model.default_parameter_values +param.process_model(pybamm_model) +param.process_geometry(geometry) + +# create mesh +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 31, var.x_s: 11, var.x_p: 31, var.r_n: 11, var.r_p: 11} +mesh = pybamm.Mesh(geometry, pybamm_model.default_submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, pybamm_model.default_spatial_methods) +disc.process_model(pybamm_model) + +# discharge timescale +tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) + +# solve model at comsol times +time = comsol_variables["time"] / tau.evaluate(0) +solution = pybamm_model.default_solver.solve(pybamm_model, time) + +"-----------------------------------------------------------------------------" +"Make Comsol 'model' for comparison" + +whole_cell = ["negative electrode", "separator", "positive electrode"] +comsol_t = comsol_variables["time"] +L_x = param.evaluate(pybamm.standard_parameters_lithium_ion.L_x) + + +def get_interp_fun(variable, domain): + """ + Create a :class:`pybamm.Function` object using the variable, to allow plotting with + :class:`'pybamm.QuickPlot'` (interpolate in space to match edges, and then create + function to interpolate in time) + """ + if domain == ["negative electrode"]: + comsol_x = comsol_variables["x_n"] + elif domain == ["separator"]: + comsol_x = comsol_variables["x_s"] + elif domain == ["positive electrode"]: + comsol_x = comsol_variables["x_p"] + elif domain == whole_cell: + comsol_x = comsol_variables["x"] + # Make sure to use dimensional space + pybamm_x = mesh.combine_submeshes(*domain)[0].nodes * L_x + variable = interp.interp1d(comsol_x, variable, axis=0)(pybamm_x) + + def myinterp(t): + return interp.interp1d(comsol_t, variable)(t)[:, np.newaxis] + + # Make sure to use dimensional time + fun = pybamm.Function(myinterp, pybamm.t * tau) + fun.domain = domain + return fun + + +comsol_c_n_surf = get_interp_fun(comsol_variables["c_n_surf"], ["negative electrode"]) +comsol_c_e = get_interp_fun(comsol_variables["c_e"], whole_cell) +comsol_c_p_surf = get_interp_fun(comsol_variables["c_p_surf"], ["positive electrode"]) +comsol_phi_n = get_interp_fun(comsol_variables["phi_n"], ["negative electrode"]) +comsol_phi_e = get_interp_fun(comsol_variables["phi_e"], whole_cell) +comsol_phi_p = get_interp_fun(comsol_variables["phi_p"], ["positive electrode"]) +comsol_voltage = interp.interp1d(comsol_t, comsol_variables["voltage"]) +comsol_temperature = get_interp_fun(comsol_variables["temperature"], whole_cell) +comsol_q_irrev_n = get_interp_fun(comsol_variables["Q_irrev_n"], ["negative electrode"]) +comsol_q_irrev_p = get_interp_fun(comsol_variables["Q_irrev_p"], ["positive electrode"]) +comsol_q_rev_n = get_interp_fun(comsol_variables["Q_rev_n"], ["negative electrode"]) +comsol_q_rev_p = get_interp_fun(comsol_variables["Q_rev_p"], ["positive electrode"]) +comsol_q_total_n = get_interp_fun(comsol_variables["Q_total_n"], ["negative electrode"]) +comsol_q_total_s = get_interp_fun(comsol_variables["Q_total_s"], ["separator"]) +comsol_q_total_p = get_interp_fun(comsol_variables["Q_total_p"], ["positive electrode"]) + +# Create comsol model with dictionary of Matrix variables +comsol_model = pybamm.BaseModel() +comsol_model.variables = { + "Negative particle surface concentration [mol.m-3]": comsol_c_n_surf, + "Electrolyte concentration [mol.m-3]": comsol_c_e, + "Positive particle surface concentration [mol.m-3]": comsol_c_p_surf, + "Current [A]": pybamm_model.variables["Current [A]"], + "Negative electrode potential [V]": comsol_phi_n, + "Electrolyte potential [V]": comsol_phi_e, + "Positive electrode potential [V]": comsol_phi_p, + "Terminal voltage [V]": pybamm.Function(comsol_voltage, pybamm.t * tau), + "Cell temperature [K]": comsol_temperature, +} + +"-----------------------------------------------------------------------------" +"Plot comparison" + +#TODO: fix QuickPlot +for var in comsol_model.variables.keys(): + plot = pybamm.QuickPlot( + [pybamm_model, comsol_model], + mesh, + [solution, solution], + output_variables=[var], + labels=["PyBaMM", "Comsol"], + ) + plot.dynamic_plot(testing=True) +plt.show() diff --git a/results/comsol_comparison/load_comsol_data.py b/results/comsol_comparison/load_comsol_data.py new file mode 100644 index 0000000000..ea6f8366e0 --- /dev/null +++ b/results/comsol_comparison/load_comsol_data.py @@ -0,0 +1,138 @@ +import pybamm +import os +import pandas as pd +import pickle +import numpy as np + +# change working directory the root of pybamm +os.chdir(pybamm.root_dir()) + +# set filepath for data and name of file to pickle to +path = "input/comsol_results_csv/thermal/1C/" +savefile = "input/comsol_results/comsol_thermal_1C.pickle" + +# time-voltage +comsol = pd.read_csv(path + "voltage.csv", sep=",", header=None) +comsol_time = comsol[0].values +comsol_time_npts = len(comsol_time) +comsol_voltage = comsol[1].values + +# negative electrode potential +comsol = pd.read_csv(path + "phi_s_n.csv", sep=",", header=None) +comsol_x_n_npts = int(len(comsol[0].values) / comsol_time_npts) +comsol_x_n = comsol[0].values[0:comsol_x_n_npts] +comsol_phi_n_vals = np.reshape( + comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" +) + +# negative particle surface concentration +comsol = pd.read_csv(path + "c_n_surf.csv", sep=",", header=None) +comsol_c_n_surf_vals = np.reshape( + comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" +) + +# positive electrode potential +comsol = pd.read_csv(path + "phi_s_p.csv", sep=",", header=None) +comsol_x_p_npts = int(len(comsol[0].values) / comsol_time_npts) +comsol_x_p = comsol[0].values[0:comsol_x_p_npts] +comsol_phi_p_vals = np.reshape( + comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" +) + +# positive particle surface concentration +comsol = pd.read_csv(path + "c_p_surf.csv", sep=",", header=None) +comsol_c_p_surf_vals = np.reshape( + comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" +) + +# electrolyte concentration +comsol = pd.read_csv(path + "c_e.csv", sep=",", header=None) +comsol_x_npts = int(len(comsol[0].values) / comsol_time_npts) +comsol_x = comsol[0].values[0:comsol_x_npts] +comsol_c_e_vals = np.reshape( + comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" +) + +# electrolyte potential +comsol = pd.read_csv(path + "phi_e.csv", sep=",", header=None) +comsol_phi_e_vals = np.reshape( + comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" +) + +# temperature +comsol = pd.read_csv(path + "temperature.csv", sep=",", header=None) +comsol_temp_vals = np.reshape( + comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" +) + +# irreversible heat source in negative electrode +comsol = pd.read_csv(path + "q_irrev_n.csv", sep=",", header=None) +comsol_q_irrev_n_vals = np.reshape( + comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" +) + +# irreversible heat source in positive electrode +comsol = pd.read_csv(path + "q_irrev_p.csv", sep=",", header=None) +comsol_q_irrev_p_vals = np.reshape( + comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" +) + +# reversible heat source in negative electrode +comsol = pd.read_csv(path + "q_rev_n.csv", sep=",", header=None) +comsol_q_rev_n_vals = np.reshape( + comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" +) + +# reversible heat source in positive electrode +comsol = pd.read_csv(path + "q_rev_p.csv", sep=",", header=None) +comsol_q_rev_p_vals = np.reshape( + comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" +) + +# total heat source in negative electrode +comsol = pd.read_csv(path + "q_total_n.csv", sep=",", header=None) +comsol_q_total_n_vals = np.reshape( + comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" +) + +# total heat source in separator +comsol = pd.read_csv(path + "q_total_s.csv", sep=",", header=None) +comsol_x_s_npts = int(len(comsol[0].values) / comsol_time_npts) +comsol_x_s = comsol[0].values[0:comsol_x_s_npts] +comsol_q_total_s_vals = np.reshape( + comsol[1].values, (comsol_x_s_npts, comsol_time_npts), order="F" +) + +# total heat source in positive electrode +comsol = pd.read_csv(path + "q_total_p.csv", sep=",", header=None) +comsol_q_total_p_vals = np.reshape( + comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" +) + + +# add comsol variables to dict and pickle +comsol_variables = { + "time": comsol_time, + "x_n": comsol_x_n, + "x_s": comsol_x_s, + "x_p": comsol_x_p, + "x": comsol_x, + "voltage": comsol_voltage, + "phi_n": comsol_phi_n_vals, + "phi_p": comsol_phi_p_vals, + "phi_e": comsol_phi_e_vals, + "c_n_surf": comsol_c_n_surf_vals, + "c_p_surf": comsol_c_p_surf_vals, + "c_e": comsol_c_e_vals, + "temperature": comsol_temp_vals, + "Q_irrev_n": comsol_q_irrev_n_vals, + "Q_irrev_p": comsol_q_irrev_p_vals, + "Q_rev_n": comsol_q_rev_n_vals, + "Q_rev_p": comsol_q_rev_p_vals, + "Q_total_n": comsol_q_total_n_vals, + "Q_total_s": comsol_q_total_s_vals, + "Q_total_p": comsol_q_total_p_vals, +} + +with open(savefile, "wb") as f: + pickle.dump(comsol_variables, f, pickle.HIGHEST_PROTOCOL) From 7bdb2f935b62067dbd4b15e86d48cdf114dc879e Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Mon, 28 Oct 2019 18:22:03 +0000 Subject: [PATCH 060/122] #688 refined simulation class to allow for easy reseting of properties --- .../models/compare-lithium-ion.ipynb | 2 +- examples/notebooks/parameter-values.ipynb | 2 +- pybamm/simulation.py | 156 ++++++++++++------ tests/integration/test_simulation.py | 6 + tests/unit/test_simulation.py | 9 + 5 files changed, 127 insertions(+), 48 deletions(-) diff --git a/examples/notebooks/models/compare-lithium-ion.ipynb b/examples/notebooks/models/compare-lithium-ion.ipynb index 718009401b..076b1eb468 100644 --- a/examples/notebooks/models/compare-lithium-ion.ipynb +++ b/examples/notebooks/models/compare-lithium-ion.ipynb @@ -463,7 +463,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/notebooks/parameter-values.ipynb b/examples/notebooks/parameter-values.ipynb index 1a2e2b798a..0ece138286 100644 --- a/examples/notebooks/parameter-values.ipynb +++ b/examples/notebooks/parameter-values.ipynb @@ -482,7 +482,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/pybamm/simulation.py b/pybamm/simulation.py index a995bca026..3164adc62a 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -1,23 +1,80 @@ import pybamm import numpy as np +import copy class Simulation: def __init__(self, model): - - self._model = model - self._geometry = model.default_geometry - self._parameter_values = model.default_parameter_values - self._submesh_types = model.default_submesh_types - self._var_pts = model.default_var_pts - self._spatial_methods = model.default_spatial_methods - self._solver = model.default_solver + self.model = model + self.set_defaults() + self.reset() + + def set_defaults(self): + self.geometry = self._model.default_geometry + + self._parameter_values = self._model.default_parameter_values + self._submesh_types = self._model.default_submesh_types + self._var_pts = self._model.default_var_pts + self._spatial_methods = self._model.default_spatial_methods + self._solver = self._model.default_solver self._quick_plot_vars = None + def reset(self): + self.model = self._model_class(self._model_options) + self.geometry = copy.deepcopy(self._unprocessed_geometry) + self._mesh = None + self._discretization = None + self._solution = None + self._status = "Unprocessed" + + def parameterize(self): + if self._status == "Unprocessed": + self._parameter_values.process_model(self._model) + self._parameter_values.process_geometry(self._geometry) + self._status = "Parameterized" + elif self._status == "Built": + # There is a function to update parameters in the model + # but this misses some geometric parameters. This class + # is for convenience and not speed so just re-build. + self.reset() + self.build() + + def build(self): + if self._status != "Built": + self.parameterize() + self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) + self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) + self._disc.process_model(self._model) + self._model_status = "Built" + + def solve(self, t_eval=None): + self.build() + + if t_eval is None: + t_eval = np.linspace(0, 1, 100) + + self._solution = self.solver.solve(self._model, t_eval) + + def plot(self): + plot = pybamm.QuickPlot( + self._model, self._mesh, self._solution, self._quick_plot_vars + ) + plot.dynamic_plot() + @property def model(self): return self._model + @model.setter + def model(self, model): + self._model = model + self._model_class = model.__class__ + self._model_options = model.options + + @property + def model_options(self): + return self._model_options + @property def geometry(self): return self._geometry @@ -25,6 +82,7 @@ def geometry(self): @geometry.setter def geometry(self, geometry): self._geometry = geometry + self._unprocessed_geometry = copy.deepcopy(geometry) @property def parameter_values(self): @@ -34,66 +92,72 @@ def parameter_values(self): def parameter_values(self, parameter_values): self._parameter_values = parameter_values + if self._status == "Parameterized": + self.reset() + self.parameterize() + + elif self._status == "Built": + self._parameter_values.update_model(self._model, self._disc) + @property def submesh_types(self): return self._submesh_types - @submesh_types.setter - def submesh_types(self, submesh_types): - # check of correct form - self._submesh_types = submesh_types - @property def var_pts(self): return self._var_pts - @var_pts.setter - def var_pts(self, var_pts): - self._var_pts = var_pts - @property def spatial_methods(self): return self._spatial_methods - @spatial_methods.setter - def spatial_methods(self, spatial_methods): - self._spatial_methods = spatial_methods - @property def solver(self): return self._solver - @solver.setter - def solver(self, solver): - self._solver = solver - @property def quick_plot_vars(self): return self._quick_plot_vars - @quick_plot_vars.setter - def quick_plot_vars(self, quick_plot_vars): - self._quick_plot_vars = quick_plot_vars - @property def solution(self): return self._solution - def solve(self, t_eval=None): - self._parameter_values.process_model(self._model) - self._parameter_values.process_geometry(self._geometry) - self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) - self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) - self._disc.process_model(self._model) - - if t_eval is None: - t_eval = np.linspace(0, 1, 100) - - self._solution = self.solver.solve(self._model, t_eval) - - def plot(self): - plot = pybamm.QuickPlot( - self._model, self._mesh, self._solution, self._quick_plot_vars - ) - plot.dynamic_plot() + def set_specs( + self, + model_options=None, + geometry=None, + parameter_values=None, + submesh_types=None, + var_pts=None, + spatial_methods=None, + solver=None, + quick_plot_vars=None, + ): + + if model_options: + self._model_options = model_options + + if geometry: + self.geometry = geometry + + if parameter_values: + self._parameter_values = parameter_values + if submesh_types: + self._submesh_types = submesh_types + if var_pts: + self._var_pts = var_pts + if spatial_methods: + self._spatial_methods = spatial_methods + if solver: + self._solver = solver + if quick_plot_vars: + self._quick_plot_vars = quick_plot_vars + + if self._status == "Parameterized": + self.reset() + self.parameterize + elif self._status == "Built": + self.reset() + self.build() diff --git a/tests/integration/test_simulation.py b/tests/integration/test_simulation.py index a3632f21bb..a7ce9f02a6 100644 --- a/tests/integration/test_simulation.py +++ b/tests/integration/test_simulation.py @@ -8,6 +8,12 @@ def test_run_with_spm(self): sim = pybamm.Simulation(model) sim.solve() + def test_update_parameters(self): + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model) + + sim.parameterize_model() + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 605913560d..450c0e5bd5 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -10,3 +10,12 @@ def test_set_model(self): self.assertEqual(sim.model, model) + def test_reset_model(self): + + sim = pybamm.Simulation(pybamm.SPM()) + + sim.discretize_model() + + sim.reset_model() + + # check can now re-parameterize model From ec7e934dad4cb534103b2c38d639154b6f096455 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Mon, 28 Oct 2019 22:41:18 -0400 Subject: [PATCH 061/122] #684 set up stepping --- pybamm/solvers/algebraic_solver.py | 4 +- pybamm/solvers/base_solver.py | 7 +- pybamm/solvers/casadi_solver.py | 64 ++++++++++++++++++- pybamm/solvers/idaklu_solver.py | 1 + pybamm/solvers/scikits_dae_solver.py | 1 + pybamm/solvers/scikits_ode_solver.py | 1 + pybamm/solvers/scipy_solver.py | 1 + pybamm/solvers/solution.py | 5 ++ tests/unit/test_solvers/test_casadi_solver.py | 10 +-- .../unit/test_solvers/test_scikits_solvers.py | 10 --- tests/unit/test_solvers/test_scipy_solver.py | 2 +- 11 files changed, 82 insertions(+), 24 deletions(-) diff --git a/pybamm/solvers/algebraic_solver.py b/pybamm/solvers/algebraic_solver.py index 3f085daf06..ff69455ef9 100644 --- a/pybamm/solvers/algebraic_solver.py +++ b/pybamm/solvers/algebraic_solver.py @@ -24,6 +24,7 @@ class AlgebraicSolver(object): def __init__(self, method="lm", tol=1e-6): self.method = method self.tol = tol + self.name = "Algebraic solver ({})".format(method) @property def method(self): @@ -51,7 +52,7 @@ def solve(self, model): equations. """ - pybamm.logger.info("Start solving {}".format(model.name)) + pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) # Set up timer = pybamm.Timer() @@ -87,7 +88,6 @@ def jacobian(y): # Assign times solution.solve_time = timer.time() - solve_start_time - solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {}".format(model.name)) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 3b95b10f73..2e4c2be9c8 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -64,7 +64,7 @@ def solve(self, model, t_eval): If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - pybamm.logger.info("Start solving {}".format(model.name)) + pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: @@ -84,7 +84,6 @@ def solve(self, model, t_eval): # Assign times solution.solve_time = solve_time - solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) @@ -129,7 +128,6 @@ def step(self, model, dt, npts=2): # Run set up on first step if not hasattr(self, "y0"): - start_time = timer.time() if model.convert_to_format == "casadi" or isinstance( self, pybamm.CasadiSolver ): @@ -137,7 +135,7 @@ def step(self, model, dt, npts=2): else: self.set_up(model) self.t = 0.0 - set_up_time = timer.time() - start_time + set_up_time = timer.time() else: set_up_time = None @@ -149,7 +147,6 @@ def step(self, model, dt, npts=2): # Assign times solution.solve_time = solve_time if set_up_time: - solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time # Set self.t and self.y0 to their values at the final step diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index b12d36f17d..031a7f132a 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -21,7 +21,7 @@ class CasadiSolver(pybamm.DaeSolver): def __init__( self, - method="idas", + method="ida", rtol=1e-6, atol=1e-6, root_method="lm", @@ -31,6 +31,68 @@ def __init__( ): super().__init__(method, rtol, atol, root_method, root_tol, max_steps) self.extra_options = extra_options + self.name = "CasADi solver ({})".format(method) + + def solve(self, model, t_eval, mode="safe"): + """ + Execute the solver setup and calculate the solution of the model at + specified times. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + t_eval : numeric type + The times at which to compute the solution + mode : str + How to solve the model (default is "safe"): + + - "fast": perform direct integration, without accounting for events. \ + Recommended when simulating a drive cycle or other simulation where \ + no events should be triggered. + - "safe": perform step-and-check integration, checking whether events have \ + been triggered. Recommended for simulations of a full charge or discharge. + + Raises + ------ + :class:`pybamm.ValueError` + If an invalid mode is passed. + :class:`pybamm.ModelError` + If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) + + """ + if mode == "fast": + # Solve model normally by calling the solve method from parent class + return super().solve(model, t_eval) + elif mode == "safe": + # Step-and-check + # old_event_signs = np.sign( + # np.concatenate([event(0, y0) for event in self.events]) + # ) + timer = pybamm.Timer() + self.set_up_casadi(model) + set_up_time = timer.time() + self.t = 0.0 + solution = None + for dt in np.diff(t_eval): + current_step_sol = self.step(model, dt) + if not solution: + # create solution object on first step + solution = current_step_sol + solution.set_up_time = set_up_time + else: + # append solution from the current step to solution + solution.append(current_step_sol) + return solution + else: + raise ValueError( + """ + invalid mode '{}'. Must be either 'safe', for solving with events, + or 'fast', for solving quickly without events""".format( + mode + ) + ) def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. In this class, we diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 131f0fdd8b..d7e985d83f 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -43,6 +43,7 @@ def __init__( raise ImportError("KLU is not installed") super().__init__("ida", rtol, atol, root_method, root_tol, max_steps) + self.name = "IDA KLU solver" def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian): """ diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py index b45425e815..1e572f4af6 100644 --- a/pybamm/solvers/scikits_dae_solver.py +++ b/pybamm/solvers/scikits_dae_solver.py @@ -48,6 +48,7 @@ def __init__( raise ImportError("scikits.odes is not installed") super().__init__(method, rtol, atol, root_method, root_tol, max_steps) + self.name = "Scikits DAE solver ({})".format(method) def integrate( self, residuals, y0, t_eval, events=None, mass_matrix=None, jacobian=None diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index 542b15d19a..6fbe70127d 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -40,6 +40,7 @@ def __init__(self, method="cvode", rtol=1e-6, atol=1e-6, linsolver="dense"): super().__init__(method, rtol, atol) self.linsolver = linsolver + self.name = "Scikits ODE solver ({})".format(method) def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index 23e11483c5..fd2af8d763 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -22,6 +22,7 @@ class ScipySolver(pybamm.OdeSolver): def __init__(self, method="BDF", rtol=1e-6, atol=1e-6): super().__init__(method, rtol, atol) + self.name = "Scipy solver ({})".format(method) def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index d185e5e450..a25d53d1a5 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -93,3 +93,8 @@ def append(self, solution): """ self.t = np.concatenate((self.t, solution.t[1:])) self.y = np.concatenate((self.y, solution.y[:, 1:]), axis=1) + self.solve_time += solution.solve_time + + @property + def total_time(self): + return self.set_up_time + self.solve_time diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 6d6b45eecd..3cc6377e17 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -60,6 +60,11 @@ def test_integrate_failure(self): # Turn warnings back on warnings.simplefilter("default") + def test_bad_mode(self): + solver = pybamm.CasadiSolver() + with self.assertRaisesRegex(ValueError, "invalid mode"): + solver.solve(None, None, "bad mode") + def test_model_solver(self): # Create model model = pybamm.BaseModel() @@ -81,11 +86,6 @@ def test_model_solver(self): np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - def test_model_step(self): # Create model model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index c0977fd709..b264921202 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -411,11 +411,6 @@ def test_model_solver_ode(self): np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - def test_model_solver_ode_events(self): # Create model model = pybamm.BaseModel() @@ -508,11 +503,6 @@ def test_model_solver_dae(self): np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - def test_model_solver_dae_bad_ics(self): # Create model model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index f5d7e783ce..c083dbabc7 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -154,7 +154,7 @@ def test_model_solver(self): np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) # Test time - self.assertGreater( + self.assertEqual( solution.total_time, solution.solve_time + solution.set_up_time ) From 3a28b08fcdb12f15d779313aa1050f7c7e38f2e0 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Mon, 28 Oct 2019 23:40:55 -0400 Subject: [PATCH 062/122] #684 add events --- pybamm/solvers/base_solver.py | 16 +++++-- pybamm/solvers/casadi_solver.py | 48 ++++++++++++++----- tests/unit/test_solvers/test_casadi_solver.py | 35 +++++++++++++- 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 2e4c2be9c8..8d4e3bc07d 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -96,7 +96,7 @@ def solve(self, model, t_eval): ) return solution - def step(self, model, dt, npts=2): + def step(self, model, dt, npts=2, log=True): """ Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by @@ -128,11 +128,18 @@ def step(self, model, dt, npts=2): # Run set up on first step if not hasattr(self, "y0"): + pybamm.logger.info( + "Start stepping {} with {}".format(model.name, self.name) + ) + if model.convert_to_format == "casadi" or isinstance( self, pybamm.CasadiSolver ): self.set_up_casadi(model) else: + pybamm.logger.debug( + "Start stepping {} with {}".format(model.name, self.name) + ) self.set_up(model) self.t = 0.0 set_up_time = timer.time() @@ -140,7 +147,6 @@ def step(self, model, dt, npts=2): set_up_time = None # Step - pybamm.logger.info("Start stepping {}".format(model.name)) t_eval = np.linspace(self.t, self.t + dt, npts) solution, solve_time, termination = self.compute_solution(model, t_eval) @@ -153,9 +159,9 @@ def step(self, model, dt, npts=2): self.t = solution.t[-1] self.y0 = solution.y[:, -1] - pybamm.logger.info("Finish stepping {} ({})".format(model.name, termination)) + pybamm.logger.debug("Finish stepping {} ({})".format(model.name, termination)) if set_up_time: - pybamm.logger.info( + pybamm.logger.debug( "Set-up time: {}, Step time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), @@ -163,7 +169,7 @@ def step(self, model, dt, npts=2): ) ) else: - pybamm.logger.info( + pybamm.logger.debug( "Step time: {}".format(timer.format(solution.solve_time)) ) return solution diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 031a7f132a..e8982dc8eb 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -21,7 +21,7 @@ class CasadiSolver(pybamm.DaeSolver): def __init__( self, - method="ida", + method="idas", rtol=1e-6, atol=1e-6, root_method="lm", @@ -65,25 +65,51 @@ def solve(self, model, t_eval, mode="safe"): if mode == "fast": # Solve model normally by calling the solve method from parent class return super().solve(model, t_eval) + elif model.events == {}: + pybamm.logger.info("No events found, running fast mode") + # Solve model normally by calling the solve method from parent class + return super().solve(model, t_eval) elif mode == "safe": # Step-and-check - # old_event_signs = np.sign( - # np.concatenate([event(0, y0) for event in self.events]) - # ) timer = pybamm.Timer() self.set_up_casadi(model) set_up_time = timer.time() + init_event_signs = np.sign( + np.concatenate([event(0, self.y0) for event in self.event_funs]) + ) self.t = 0.0 solution = None + pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) for dt in np.diff(t_eval): + # Step current_step_sol = self.step(model, dt) - if not solution: - # create solution object on first step - solution = current_step_sol - solution.set_up_time = set_up_time + # Check most recent y + new_event_signs = np.sign( + np.concatenate( + [ + event(0, current_step_sol.y[:, -1]) + for event in self.event_funs + ] + ) + ) + # Exit loop if the sign of an event changes + if (new_event_signs != init_event_signs).any(): + break else: - # append solution from the current step to solution - solution.append(current_step_sol) + if not solution: + # create solution object on first step + solution = current_step_sol + solution.set_up_time = set_up_time + else: + # append solution from the current step to solution + solution.append(current_step_sol) + pybamm.logger.info( + "Set-up time: {}, Step time: {}, Total time: {}".format( + timer.format(solution.set_up_time), + timer.format(solution.solve_time), + timer.format(solution.total_time), + ) + ) return solution else: raise ValueError( @@ -111,7 +137,7 @@ def compute_solution(self, model, t_eval): timer = pybamm.Timer() solve_start_time = timer.time() - pybamm.logger.info("Calling DAE solver") + pybamm.logger.debug("Calling DAE solver") solution = self.integrate_casadi( self.casadi_problem, self.y0, t_eval, mass_matrix=model.mass_matrix.entries ) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 3cc6377e17..6fc6f0d9bc 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -5,7 +5,7 @@ import pybamm import unittest import numpy as np -from tests import get_mesh_for_testing +from tests import get_mesh_for_testing, get_discretisation_for_testing import warnings @@ -62,8 +62,10 @@ def test_integrate_failure(self): def test_bad_mode(self): solver = pybamm.CasadiSolver() + model = pybamm.BaseModel() + model.events = {1: 2} with self.assertRaisesRegex(ValueError, "invalid mode"): - solver.solve(None, None, "bad mode") + solver.solve(model, None, "bad mode") def test_model_solver(self): # Create model @@ -86,6 +88,35 @@ def test_model_solver(self): np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + def test_model_solver_events(self): + # Create model + model = pybamm.BaseModel() + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = { + "var1 = 1.5": pybamm.min(var1 - 1.5), + "var2 = 2.5": pybamm.min(var2 - 2.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + def test_model_step(self): # Create model model = pybamm.BaseModel() From 01156446f04f721f3dd8943191a5755aa9b49ed3 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Tue, 29 Oct 2019 11:54:05 -0400 Subject: [PATCH 063/122] #684 fix tests, still need to do event termination --- examples/scripts/compare_lithium_ion.py | 4 +-- pybamm/solvers/base_solver.py | 1 + pybamm/solvers/casadi_solver.py | 36 ++++++++++++++++--- pybamm/solvers/dae_solver.py | 1 + pybamm/solvers/ode_solver.py | 1 + .../test_solvers/test_algebraic_solver.py | 5 --- 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 1aca4c7816..c61d378686 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -45,9 +45,9 @@ # solve model solutions = [None] * len(models) -t_eval = np.linspace(0, 0.17, 100) +t_eval = np.linspace(0, 0.3, 100) for i, model in enumerate(models): - solutions[i] = model.default_solver.solve(model, t_eval) + solutions[i] = pybamm.CasadiSolver().solve(model, t_eval) # plot plot = pybamm.QuickPlot(models, mesh, solutions) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 8d4e3bc07d..37f2fd819d 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -20,6 +20,7 @@ def __init__(self, method=None, rtol=1e-6, atol=1e-6): self._method = method self._rtol = rtol self._atol = atol + self.name = "Base solver" @property def method(self): diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index e8982dc8eb..c00df6e044 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -27,9 +27,11 @@ def __init__( root_method="lm", root_tol=1e-6, max_steps=1000, + max_step_decrease_count=10, **extra_options, ): super().__init__(method, rtol, atol, root_method, root_tol, max_steps) + self.max_step_decrease_count = max_step_decrease_count self.extra_options = extra_options self.name = "CasADi solver ({})".format(method) @@ -77,12 +79,38 @@ def solve(self, model, t_eval, mode="safe"): init_event_signs = np.sign( np.concatenate([event(0, self.y0) for event in self.event_funs]) ) - self.t = 0.0 solution = None - pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) + pybamm.logger.info( + "Start solving {} with {} in 'safe' mode".format(model.name, self.name) + ) for dt in np.diff(t_eval): # Step - current_step_sol = self.step(model, dt) + solved = False + count = 0 + while not solved: + # Try to solve with the current step, if it fails then halve the + # step size and try again. This will make solution.t slightly + # different to t_eval, but shouldn't matter too much as it should + # only happen near events. + try: + current_step_sol = self.step(model, dt) + solved = True + except pybamm.SolverError: + dt /= 2 + count += 1 + if count >= self.max_step_decrease_count: + if solution is None: + t = 0 + else: + t = solution.t + raise pybamm.SolverError( + """ + Maximum number of decreased steps occurred at t={}. Try + solving the model up to this time only + """.format( + t + ) + ) # Check most recent y new_event_signs = np.sign( np.concatenate( @@ -104,7 +132,7 @@ def solve(self, model, t_eval, mode="safe"): # append solution from the current step to solution solution.append(current_step_sol) pybamm.logger.info( - "Set-up time: {}, Step time: {}, Total time: {}".format( + "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 39fe18f364..74001b7b9c 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -39,6 +39,7 @@ def __init__( self.root_method = root_method self.root_tol = root_tol self.max_steps = max_steps + self.name = "Base DAE solver" @property def root_method(self): diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index 9b650241f0..a1655d745f 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -19,6 +19,7 @@ class OdeSolver(pybamm.BaseSolver): def __init__(self, method=None, rtol=1e-6, atol=1e-6): super().__init__(method, rtol, atol) + self.name = "Base ODE solver" def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 629eb29522..772426bb6d 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -107,11 +107,6 @@ def test_model_solver(self): model.variables["var2"].evaluate(t=None, y=solution.y), sol[100:] ) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - # Test without jacobian model.use_jacobian = False solution_no_jac = solver.solve(model) From ca01dd7dabb9d717e4ab90d3394b0dc97007fff6 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 29 Oct 2019 17:23:49 +0000 Subject: [PATCH 064/122] #678 fix cooling --- examples/notebooks/models/SPM.ipynb | 12 +- examples/scripts/thermal_lithium_ion.py | 5 +- .../models/submodels/thermal/base_thermal.py | 249 ++++++++++++++---- .../thermal/isothermal/isothermal.py | 12 +- .../x_full/x_full_no_current_collector.py | 6 +- .../thermal/x_lumped/base_x_lumped.py | 8 + .../x_lumped_0D_current_collectors.py | 8 +- .../x_lumped_1D_current_collectors.py | 9 +- .../x_lumped_2D_current_collectors.py | 5 +- .../x_lumped_no_current_collectors.py | 46 +++- .../xyz_lumped_1D_current_collector.py | 1 + .../xyz_lumped_2D_current_collector.py | 1 + pybamm/parameters/geometric_parameters.py | 4 +- .../standard_parameters_lithium_ion.py | 3 +- pybamm/parameters/thermal_parameters.py | 16 +- .../compare_comsol_thermal.py | 215 ++++++++++++++- results/comsol_comparison/load_comsol_data.py | 4 + 17 files changed, 499 insertions(+), 105 deletions(-) diff --git a/examples/notebooks/models/SPM.ipynb b/examples/notebooks/models/SPM.ipynb index 54bf98ee01..47a3dd06c0 100644 --- a/examples/notebooks/models/SPM.ipynb +++ b/examples/notebooks/models/SPM.ipynb @@ -638,17 +638,17 @@ "\t- Local current collector potential difference\n", "\t- Local current collector potential difference [V]\n", "\t- Ohmic heating\n", - "\t- Ohmic heating [A.V.m-3]\n", + "\t- Ohmic heating [W.m-3]\n", "\t- Irreversible electrochemical heating\n", - "\t- Irreversible electrochemical heating [A.V.m-3]\n", + "\t- Irreversible electrochemical heating [W.m-3]\n", "\t- Reversible heating\n", - "\t- Reversible heating [A.V.m-3]\n", + "\t- Reversible heating [W.m-3]\n", "\t- Total heating\n", - "\t- Total heating [A.V.m-3]\n", + "\t- Total heating [W.m-3]\n", "\t- X-averaged total heating\n", - "\t- X-averaged total heating [A.V.m-3]\n", + "\t- X-averaged total heating [W.m-3]\n", "\t- Volume-averaged total heating\n", - "\t- Volume-averaged total heating [A.V.m-3]\n", + "\t- Volume-averaged total heating [W.m-3]\n", "\t- Positive current collector potential [V]\n", "\t- Local potential difference\n", "\t- Local potential difference [V]\n", diff --git a/examples/scripts/thermal_lithium_ion.py b/examples/scripts/thermal_lithium_ion.py index 13f3826eb2..79747040d8 100644 --- a/examples/scripts/thermal_lithium_ion.py +++ b/examples/scripts/thermal_lithium_ion.py @@ -18,7 +18,7 @@ # load parameter values and process models and geometry param = models[0].default_parameter_values -param.update({"Heat transfer coefficient [W.m-2.K-1]": 0.1}) +param.update({"Heat transfer coefficient [W.m-2.K-1]": 1}) for model in models: param.process_model(model) @@ -40,7 +40,8 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): - solution = model.default_solver.solve(model, t_eval) + solver = pybamm.ScipySolver(atol=1e-8, rtol=1e-8) + solution = solver.solve(model, t_eval) solutions[i] = solution # plot diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index e2b801db14..4495694c82 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -92,16 +92,31 @@ def _get_standard_coupled_variables(self, variables): phi_s_n = variables["Negative electrode potential"] phi_s_p = variables["Positive electrode potential"] + # Ohmic heating in solid Q_ohm_s_cn, Q_ohm_s_cp = self._current_collector_heating(variables) Q_ohm_s_n = -pybamm.inner(i_s_n, pybamm.grad(phi_s_n)) Q_ohm_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") Q_ohm_s_p = -pybamm.inner(i_s_p, pybamm.grad(phi_s_p)) Q_ohm_s = pybamm.Concatenation(Q_ohm_s_n, Q_ohm_s_s, Q_ohm_s_p) - Q_ohm_e = -pybamm.inner(i_e, pybamm.grad(phi_e)) + # Ohmic heating in electrolyte + # TODO: change full stefan-maxwell conductivity so that i_e is always + # a Concatenation + if isinstance(i_e, pybamm.Concatenation): + # compute by domain if possible + i_e_n, i_e_s, i_e_p = i_e.orphans + phi_e_n, phi_e_s, phi_e_p = phi_e.orphans + Q_ohm_e_n = -pybamm.inner(i_e_n, pybamm.grad(phi_e_n)) + Q_ohm_e_s = -pybamm.inner(i_e_s, pybamm.grad(phi_e_s)) + Q_ohm_e_p = -pybamm.inner(i_e_p, pybamm.grad(phi_e_p)) + Q_ohm_e = pybamm.Concatenation(Q_ohm_e_n, Q_ohm_e_s, Q_ohm_e_p) + else: + Q_ohm_e = -pybamm.inner(i_e, pybamm.grad(phi_e)) + # Total Ohmic heating Q_ohm = Q_ohm_s + Q_ohm_e + # Irreversible electrochemical heating Q_rxn_n = j_n * eta_r_n Q_rxn_p = j_p * eta_r_p Q_rxn = pybamm.Concatenation( @@ -112,6 +127,7 @@ def _get_standard_coupled_variables(self, variables): ] ) + # Reversible electrochemical heating Q_rev_n = j_n * (param.Theta ** (-1) + T_n) * dUdT_n Q_rev_p = j_p * (param.Theta ** (-1) + T_p) * dUdT_p Q_rev = pybamm.Concatenation( @@ -122,6 +138,7 @@ def _get_standard_coupled_variables(self, variables): ] ) + # Total heating Q = Q_ohm + Q_rxn + Q_rev # Compute the X-average over the current collectors by default. @@ -133,77 +150,194 @@ def _get_standard_coupled_variables(self, variables): Q_rev_av = self._x_average(Q_rev, 0, 0) Q_av = self._x_average(Q, Q_ohm_s_cn, Q_ohm_s_cp) + # Compute volume-averaged heat source terms Q_ohm_vol_av = self._yz_average(Q_ohm_av) Q_rxn_vol_av = self._yz_average(Q_rxn_av) Q_rev_vol_av = self._yz_average(Q_rev_av) Q_vol_av = self._yz_average(Q_av) + # Dimensional scaling for heat source terms + Q_scale = param.i_typ * param.potential_scale / param.L_x + + # Geat heat source by domain + if isinstance(Q_ohm_e, pybamm.Concatenation): + variables.update( + self._get_domain_heat_source_terms(Q_ohm_s, Q_ohm_e, Q_rxn, Q_rev) + ) + variables.update( { "Ohmic heating": Q_ohm, - "Ohmic heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_ohm - / param.L_x, + "Ohmic heating [W.m-3]": Q_ohm * Q_scale, "X-averaged Ohmic heating": Q_ohm_av, - "X-averaged Ohmic heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_ohm_av - / param.L_x, + "X-averaged Ohmic heating [W.m-3]": Q_ohm_av * Q_scale, "Volume-averaged Ohmic heating": Q_ohm_vol_av, - "Volume-averaged Ohmic heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_ohm_vol_av - / param.L_x, + "Volume-averaged Ohmic heating [W.m-3]": Q_ohm_vol_av * Q_scale, "Irreversible electrochemical heating": Q_rxn, - "Irreversible electrochemical heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rxn - / param.L_x, - "X-averaged electrochemical heating": Q_rxn_av, - "X-averaged electrochemical heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rxn_av - / param.L_x, - "Volume-averaged electrochemical heating": Q_rxn_vol_av, - "Volume-averaged electrochemical heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rxn_vol_av - / param.L_x, + "Irreversible electrochemical heating [W.m-3]": Q_rxn * Q_scale, + "X-averaged irreversible electrochemical heating": Q_rxn_av, + "X-averaged irreversible electrochemical heating [W.m-3]": Q_rxn_av + * Q_scale, + "Volume-averaged irreversible electrochemical heating": Q_rxn_vol_av, + "Volume-averaged irreversible electrochemical heating [W.m-3]": Q_rxn_vol_av + * Q_scale, "Reversible heating": Q_rev, - "Reversible heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rev - / param.L_x, + "Reversible heating [W.m-3]": Q_rev * Q_scale, "X-averaged reversible heating": Q_rev_av, - "X-averaged reversible heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rev_av - / param.L_x, + "X-averaged reversible heating [W.m-3]": Q_rev_av * Q_scale, "Volume-averaged reversible heating": Q_rev_vol_av, - "Volume-averaged reversible heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rev_vol_av - / param.L_x, + "Volume-averaged reversible heating [W.m-3]": Q_rev_vol_av * Q_scale, "Total heating": Q, - "Total heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q - / param.L_x, + "Total heating [W.m-3]": Q * Q_scale, "X-averaged total heating": Q_av, - "X-averaged total heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_av - / param.L_x, + "X-averaged total heating [W.m-3]": Q_av * Q_scale, "Volume-averaged total heating": Q_vol_av, - "Volume-averaged total heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_vol_av - / param.L_x, + "Volume-averaged total heating [W.m-3]": Q_vol_av * Q_scale, } ) return variables + def _get_domain_heat_source_terms(self, Q_ohm_s, Q_ohm_e, Q_rxn, Q_rev): + """ + A private function to obtain the heat source terms split + by domain: 'negative electrode', 'separator' and 'positive electrode'. + + Parameters + ---------- + Q_ohm_s : :class:`pybamm.Symbol` + The Ohmic heating in the solid. + Q_ohm_e : :class:`pybamm.Symbol` + The Ohmic heating in the electrolyte. + Q_rxn : :class:`pybamm.Symbol` + The irreversible electrochemical heating. + Q_ohm_s : :class:`pybamm.Symbol` + The reversible electrochemical heating. + + Returns + ------- + variables : dict + The variables corresponding to the heat source terms in the + separate domains. + """ + param = self.param + + # Unpack by domain + Q_ohm_s_n, _, Q_ohm_s_p = Q_ohm_s.orphans + Q_ohm_e_n, Q_ohm_e_s, Q_ohm_e_p = Q_ohm_e.orphans + Q_ohm_n = Q_ohm_s_n + Q_ohm_e_n + Q_ohm_p = Q_ohm_s_p + Q_ohm_e_p + Q_rxn_n, _, Q_rxn_p = Q_rxn.orphans + Q_rev_n, _, Q_rev_p = Q_rev.orphans + Q_n = Q_ohm_n + Q_rxn_n + Q_rev_n + Q_s = Q_ohm_e_s + Q_p = Q_ohm_p + Q_rxn_p + Q_rev_p + + # X-average + Q_ohm_n_av = pybamm.x_average(Q_ohm_n) + Q_ohm_s_av = pybamm.x_average(Q_ohm_e_s) + Q_ohm_p_av = pybamm.x_average(Q_ohm_p) + Q_rxn_n_av = pybamm.x_average(Q_rxn_n) + Q_rxn_p_av = pybamm.x_average(Q_rxn_n) + Q_rev_n_av = pybamm.x_average(Q_rev_n) + Q_rev_p_av = pybamm.x_average(Q_rev_n) + Q_n_av = pybamm.x_average(Q_n) + Q_s_av = pybamm.x_average(Q_s) + Q_p_av = pybamm.x_average(Q_p) + + # Volume average + Q_ohm_n_vol_av = self._yz_average(Q_ohm_n_av) + Q_ohm_s_vol_av = self._yz_average(Q_ohm_s_av) + Q_ohm_p_vol_av = self._yz_average(Q_ohm_p_av) + Q_rxn_n_vol_av = self._yz_average(Q_rxn_n_av) + Q_rxn_p_vol_av = self._yz_average(Q_rxn_p_av) + Q_rev_n_vol_av = self._yz_average(Q_rev_n_av) + Q_rev_p_vol_av = self._yz_average(Q_rev_p_av) + Q_n_vol_av = self._yz_average(Q_n_av) + Q_s_vol_av = self._yz_average(Q_s_av) + Q_p_vol_av = self._yz_average(Q_p_av) + + # Dimensional scaling for heat source terms + Q_scale = param.i_typ * param.potential_scale / param.L_x + + variables = { + "Negative electrode Ohmic heating": Q_ohm_n, + "Negative electrode Ohmic heating [W.m-3]": Q_ohm_n * Q_scale, + "X-averaged negative electrode Ohmic heating": Q_ohm_n_av, + "X-averaged negative electrode Ohmic heating [W.m-3]": Q_ohm_n_av * Q_scale, + "Volume-averaged negative electrode ohm heating": Q_ohm_n_vol_av, + "Volume-averaged negative electrode ohm heating [W.m-3]": Q_ohm_n_vol_av + * Q_scale, + "Separator Ohmic heating": Q_ohm_s, + "Separator Ohmic heating [W.m-3]": Q_ohm_s * Q_scale, + "X-averaged separator Ohmic heating": Q_ohm_s_av, + "X-averaged separator Ohmic heating [W.m-3]": Q_ohm_s_av * Q_scale, + "Volume-averaged separator ohm heating": Q_ohm_s_vol_av, + "Volume-averaged separator ohm heating [W.m-3]": Q_ohm_s_vol_av * Q_scale, + "Positive electrode Ohmic heating": Q_ohm_p, + "Positive electrode Ohmic heating [W.m-3]": Q_ohm_p * Q_scale, + "X-averaged positive electrode Ohmic heating": Q_ohm_p_av, + "X-averaged positive electrode Ohmic heating [W.m-3]": Q_ohm_p_av * Q_scale, + "Volume-averaged positive electrode Ohmic heating": Q_ohm_p_vol_av, + "Volume-averaged positive electrode Ohmic heating [W.m-3]": Q_ohm_p_vol_av + * Q_scale, + "Negative electrode irreversible electrochemical heating": Q_rxn_n_av, + "Negative electrode irreversible electrochemical heating [W.m-3]": Q_rxn_n_av + * Q_scale, + "X-averaged negative electrode irreversible electrochemical heating": Q_rxn_n_av, + "X-averaged negative electrode irreversible electrochemical heating [W.m-3]": Q_rxn_n_av + * Q_scale, + "Volume-averaged negative electrode irreversible electrochemical heating": Q_rxn_n_vol_av, + "Volume-averaged negative electrode irreversible electrochemical heating [W.m-3]": Q_rxn_n_vol_av + * Q_scale, + "Positive electrode irreversible electrochemical heating": Q_rxn_p_av, + "Positive electrode irreversible electrochemical heating [W.m-3]": Q_rxn_p_av + * Q_scale, + "X-averaged positive electrode irreversible electrochemical heating": Q_rxn_p_av, + "X-averaged positive electrode irreversible electrochemical heating [W.m-3]": Q_rxn_p_av + * Q_scale, + "Volume-averaged positive electrode irreversible electrochemical heating": Q_rxn_p_vol_av, + "Volume-averaged positive electrode irreversible electrochemical heating [W.m-3]": Q_rxn_p_vol_av + * Q_scale, + "Negative electrode reversible heating": Q_rev_n, + "Negative electrode reversible heating [W.m-3]": Q_rev_n * Q_scale, + "X-averaged negative electrode reversible heating": Q_rev_n_av, + "X-averaged negative electrode reversible heating [W.m-3]": Q_rev_n_av + * Q_scale, + "Volume-averaged negative electrode reversible heating": Q_rev_n_vol_av, + "Volume-averaged negative electrode reversible heating [W.m-3]": Q_rev_n_vol_av + * Q_scale, + "Positive electrode reversible heating": Q_rev_p, + "Positive electrode reversible heating [W.m-3]": Q_rev_p * Q_scale, + "X-averaged positive electrode reversible heating": Q_rev_p_av, + "X-averaged positive electrode reversible heating [W.m-3]": Q_rev_p_av + * Q_scale, + "Volume-averaged positive electrode reversible heating": Q_rev_p_vol_av, + "Volume-averaged positive electrode reversible heating [W.m-3]": Q_rev_p_vol_av + * Q_scale, + "Negative electrode total heating": Q_n, + "Negative electrode total heating [W.m-3]": Q_n * Q_scale, + "X-averaged negative electrode total heating": Q_n_av, + "X-averaged negative electrode total heating [W.m-3]": Q_n_av * Q_scale, + "Volume-averaged negative electrode total heating": Q_n_vol_av, + "Volume-averaged negative electrode total heating [W.m-3]": Q_n_vol_av + * Q_scale, + "Separator total heating": Q_s, + "Separator total heating [W.m-3]": Q_s * Q_scale, + "X-averaged separator total heating": Q_s_av, + "X-averaged separator total heating [W.m-3]": Q_s_av * Q_scale, + "Volume-averaged separator total heating": Q_s_vol_av, + "Volume-averaged separator total heating [W.m-3]": Q_s_vol_av * Q_scale, + "Positive electrode total heating": Q_p, + "Positive electrode total heating [W.m-3]": Q_p * Q_scale, + "X-averaged postive electrode total heating": Q_p_av, + "X-averaged postive electrode total heating [W.m-3]": Q_p_av * Q_scale, + "Volume-averaged postive electrode total heating": Q_p_vol_av, + "Volume-averaged positive electrode total heating [W.m-3]": Q_p_vol_av + * Q_scale, + } + + return variables + def _flux_law(self, T): raise NotImplementedError @@ -252,3 +386,16 @@ def _x_average(self, var, var_cn, var_cp): + self.param.l_cp * var_cp ) / self.param.l return out + + def _effective_properties(self): + """ + Computes the effective effective product of density and specific heat, and + effective thermal conductivity, respectively. These are computed differently + depending upon whether current collectors are included or not. Defualt + behaviour is to assume the presence of current collectors. Due to the choice + of non-dimensionalisation, the dimensionless effective properties are equal + to 1 in the case where current collectors are accounted for. + """ + rho_eff = pybamm.Scalar(1) + lambda_eff = pybamm.Scalar(1) + return rho_eff, lambda_eff diff --git a/pybamm/models/submodels/thermal/isothermal/isothermal.py b/pybamm/models/submodels/thermal/isothermal/isothermal.py index 20dc2721ba..f8ecdfc5ab 100644 --- a/pybamm/models/submodels/thermal/isothermal/isothermal.py +++ b/pybamm/models/submodels/thermal/isothermal/isothermal.py @@ -39,17 +39,17 @@ def get_coupled_variables(self, variables): variables.update( { "Ohmic heating": pybamm.Scalar(0), - "Ohmic heating [A.V.m-3]": pybamm.Scalar(0), + "Ohmic heating [W.m-3]": pybamm.Scalar(0), "Irreversible electrochemical heating": pybamm.Scalar(0), - "Irreversible electrochemical heating [A.V.m-3]": pybamm.Scalar(0), + "Irreversible electrochemical heating [W.m-3]": pybamm.Scalar(0), "Reversible heating": pybamm.Scalar(0), - "Reversible heating [A.V.m-3]": pybamm.Scalar(0), + "Reversible heating [W.m-3]": pybamm.Scalar(0), "Total heating": pybamm.Scalar(0), - "Total heating [A.V.m-3]": pybamm.Scalar(0), + "Total heating [W.m-3]": pybamm.Scalar(0), "X-averaged total heating": pybamm.Scalar(0), - "X-averaged total heating [A.V.m-3]": pybamm.Scalar(0), + "X-averaged total heating [W.m-3]": pybamm.Scalar(0), "Volume-averaged total heating": pybamm.Scalar(0), - "Volume-averaged total heating [A.V.m-3]": pybamm.Scalar(0), + "Volume-averaged total heating [W.m-3]": pybamm.Scalar(0), } ) return variables diff --git a/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py b/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py index 9d81bf76ca..a919e6003b 100644 --- a/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py +++ b/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py @@ -50,8 +50,10 @@ def _current_collector_heating(self, variables): return Q_s_cn, Q_s_cp def _yz_average(self, var): - """Computes the y-z average by integration over y and z - In this case this is just equal to the input variable""" + """ + Computes the y-z average by integration over y and z + In this case this is just equal to the input variable + """ return var def _x_average(self, var, var_cn, var_cp): diff --git a/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py b/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py index 3114a2ceb2..fa93592e60 100644 --- a/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py +++ b/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py @@ -51,3 +51,11 @@ def _flux_law(self, T): def set_initial_conditions(self, variables): T = variables["X-averaged cell temperature"] self.initial_conditions = {T: self.param.T_init} + + def _surface_cooling_coefficient(self): + """Returns the surface cooling coefficient in for x-lumped models.""" + # Account for surface area to volume ratio in cooling coefficient + # Note: assumes pouch cell geometry + A = self.param.l_y * self.param.l_z + V = self.param.l * self.param.l_y * self.param.l_z + return -2 * self.param.h * A / V / (self.param.delta ** 2) diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py index d705c0d684..1d2025a16a 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py @@ -14,12 +14,10 @@ def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + cooling_coeff = self._surface_cooling_coefficient() + self.rhs = { - T_av: ( - self.param.B * Q_av - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) * T_av - ) - / self.param.C_th + T_av: (self.param.B * Q_av + cooling_coeff * T_av) / self.param.C_th } def _current_collector_heating(self, variables): diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py index 71599b3203..fa96d98e70 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py @@ -15,12 +15,11 @@ def __init__(self, param): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + + cooling_coeff = self._surface_cooling_coefficient() + self.rhs = { - T_av: ( - pybamm.laplacian(T_av) - + self.param.B * Q_av - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) * T_av - ) + T_av: (pybamm.laplacian(T_av) + self.param.B * Q_av + cooling_coeff * T_av) / self.param.C_th } diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py index a8117bd5b5..b9d301cc14 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py @@ -16,6 +16,8 @@ def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + cooling_coeff = self._surface_cooling_coefficient() + # Add boundary source term which accounts for surface cooling around # the edge of the domain in the weak formulation. # TODO: update to allow different cooling conditions at the tabs @@ -23,8 +25,7 @@ def set_rhs(self, variables): T_av: ( pybamm.laplacian(T_av) + self.param.B * pybamm.source(Q_av, T_av) - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) - * pybamm.source(T_av, T_av) + + cooling_coeff * pybamm.source(T_av, T_av) - (self.param.h / self.param.delta) * pybamm.source(T_av, T_av, boundary=True) ) diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py index 1e35fd4b88..db66a8f36f 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py @@ -28,12 +28,13 @@ def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + # Get effective properties + rho_eff, _ = self._effective_properties() + cooling_coeff = self._surface_cooling_coefficient() + self.rhs = { - T_av: ( - self.param.B * Q_av - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) * T_av - ) - / self.param.C_th + T_av: (self.param.B * Q_av + cooling_coeff * T_av) + / (self.param.C_th * rho_eff) } def _current_collector_heating(self, variables): @@ -42,6 +43,18 @@ def _current_collector_heating(self, variables): Q_s_cp = pybamm.Scalar(0) return Q_s_cn, Q_s_cp + def _surface_cooling_coefficient(self): + """ + Returns the surface cooling coefficient in the absence of current + collectors. + """ + # Account for surface area to volume ratio in cooling coefficient + # Note: assumes pouch cell geometry and volume doesn't include current + # collectors + A = self.param.l_y * self.param.l_z + V = self.param.l_x * self.param.l_y * self.param.l_z + return -2 * self.param.h * A / V / (self.param.delta ** 2) + def _yz_average(self, var): """In 1D volume-averaged quantities are unchanged""" return var @@ -52,3 +65,26 @@ def _x_average(self, var, var_cn, var_cp): collectors. This overwrites the default behaviour of 'base_thermal'. """ return pybamm.x_average(var) + + def _effective_properties(self): + """ + Computes the effective effective product of density and specific heat, and + effective thermal conductivity, respectively. This overwrites the default + behaviour of 'base_thermal'. + """ + l_n = self.param.l_n + l_s = self.param.l_s + l_p = self.param.l_p + rho_n = self.param.rho_n + rho_s = self.param.rho_s + rho_p = self.param.rho_p + lambda_n = self.param.lambda_n + lambda_s = self.param.lambda_s + lambda_p = self.param.lambda_p + + rho_eff = (rho_n * l_n + rho_s * l_s + rho_p * l_p) / (l_n + l_n + l_p) + lambda_eff = (lambda_n * l_n + lambda_s * l_s + lambda_p * l_p) / ( + l_n + l_n + l_p + ) + + return rho_eff, lambda_eff diff --git a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py index ffca162efa..a4919709f4 100644 --- a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py +++ b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py @@ -32,6 +32,7 @@ def _current_collector_heating(self, variables): def _surface_cooling_coefficient(self): """Returns the surface cooling coefficient in 1+1D""" + # Note: assumes pouch cell geometry return ( -2 * self.param.h / (self.param.delta ** 2) / self.param.l - self.param.l_z * self.param.h / self.param.delta diff --git a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py index fcfbbd5329..bf2f9f3c5e 100644 --- a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py +++ b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py @@ -33,6 +33,7 @@ def _current_collector_heating(self, variables): def _surface_cooling_coefficient(self): """Returns the surface cooling coefficient in 2+1D""" + # Note: assumes pouch cell geometry return ( -2 * self.param.h / (self.param.delta ** 2) / self.param.l - 2 * (self.param.l_y + self.param.l_z) * self.param.h / self.param.delta diff --git a/pybamm/parameters/geometric_parameters.py b/pybamm/parameters/geometric_parameters.py index 1d7ea4780b..3b70db4865 100644 --- a/pybamm/parameters/geometric_parameters.py +++ b/pybamm/parameters/geometric_parameters.py @@ -53,11 +53,13 @@ l_s = L_s / L_x l_p = L_p / L_x l_cp = L_cp / L_x +l_x = L_x / L_x l_y = L_y / L_z l_z = L_z / L_z +a_cc = l_y * l_z l = L / L_x -delta = L_x / L_z +delta = L_x / L_z # Aspect ratio # Tab geometry l_tab_n = L_tab_n / L_z diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index 69403e3bd2..e981b597fd 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -38,7 +38,6 @@ L = pybamm.geometric_parameters.L A_cc = pybamm.geometric_parameters.A_cc - # Tab geometry L_tab_n = pybamm.geometric_parameters.L_tab_n Centre_y_tab_n = pybamm.geometric_parameters.Centre_y_tab_n @@ -254,8 +253,10 @@ def U_p_dimensional(sto, T): l_s = pybamm.geometric_parameters.l_s l_p = pybamm.geometric_parameters.l_p l_cp = pybamm.geometric_parameters.l_cp +l_x = pybamm.geometric_parameters.l_x l_y = pybamm.geometric_parameters.l_y l_z = pybamm.geometric_parameters.l_z +a_cc = pybamm.geometric_parameters.a_cc l = pybamm.geometric_parameters.l delta = pybamm.geometric_parameters.delta diff --git a/pybamm/parameters/thermal_parameters.py b/pybamm/parameters/thermal_parameters.py index 9c08124240..4041c4f5b1 100644 --- a/pybamm/parameters/thermal_parameters.py +++ b/pybamm/parameters/thermal_parameters.py @@ -38,12 +38,7 @@ "Positive current collector thermal conductivity [W.m-1.K-1]" ) -# Thermal parameters -h_dim = pybamm.Parameter("Heat transfer coefficient [W.m-2.K-1]") -Phi_dim = pybamm.Scalar(1) # typical scale for voltage drop across cell (order 1V) -Delta_T = ( - pybamm.electrical_parameters.i_typ * Phi_dim / h_dim -) # computed from balance of typical cross-cell Ohmic heating with surface heat loss +# Effective thermal properties rho_eff_dim = ( rho_cn_dim * c_p_cn_dim * pybamm.geometric_parameters.L_cn + rho_n_dim * c_p_n_dim * pybamm.geometric_parameters.L_n @@ -59,6 +54,15 @@ + lambda_cp_dim * pybamm.geometric_parameters.L_cp ) / pybamm.geometric_parameters.L +# Cooling coefficient +h_dim = pybamm.Parameter("Heat transfer coefficient [W.m-2.K-1]") + +# Typical temperature rise +Phi_dim = pybamm.Scalar(1) # typical scale for voltage drop across cell (order 1V) +Delta_T = ( + pybamm.electrical_parameters.i_typ * Phi_dim / h_dim +) # computed from balance of typical cross-cell Ohmic heating with surface heat loss + # Activation energies E_r_n = pybamm.Parameter("Negative reaction rate activation energy [J.mol-1]") E_r_p = pybamm.Parameter("Positive reaction rate activation energy [J.mol-1]") diff --git a/results/comsol_comparison/compare_comsol_thermal.py b/results/comsol_comparison/compare_comsol_thermal.py index 7c8d5d49a2..9c47f8053b 100644 --- a/results/comsol_comparison/compare_comsol_thermal.py +++ b/results/comsol_comparison/compare_comsol_thermal.py @@ -31,7 +31,7 @@ # create mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 31, var.x_s: 11, var.x_p: 31, var.r_n: 11, var.r_p: 11} +var_pts = {var.x_n: 101, var.x_s: 51, var.x_p: 101, var.r_n: 31, var.r_p: 31} mesh = pybamm.Mesh(geometry, pybamm_model.default_submesh_types, var_pts) # discretise model @@ -39,11 +39,14 @@ disc.process_model(pybamm_model) # discharge timescale -tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) +tau = param.process_symbol( + pybamm.standard_parameters_lithium_ion.tau_discharge +).evaluate(0) # solve model at comsol times -time = comsol_variables["time"] / tau.evaluate(0) -solution = pybamm_model.default_solver.solve(pybamm_model, time) +time = comsol_variables["time"] / tau +solver = pybamm.CasadiSolver() +solution = solver.solve(pybamm_model, time) "-----------------------------------------------------------------------------" "Make Comsol 'model' for comparison" @@ -88,6 +91,9 @@ def myinterp(t): comsol_phi_p = get_interp_fun(comsol_variables["phi_p"], ["positive electrode"]) comsol_voltage = interp.interp1d(comsol_t, comsol_variables["voltage"]) comsol_temperature = get_interp_fun(comsol_variables["temperature"], whole_cell) +comsol_temperature_av = interp.interp1d( + comsol_t, comsol_variables["average temperature"] +) comsol_q_irrev_n = get_interp_fun(comsol_variables["Q_irrev_n"], ["negative electrode"]) comsol_q_irrev_p = get_interp_fun(comsol_variables["Q_irrev_p"], ["positive electrode"]) comsol_q_rev_n = get_interp_fun(comsol_variables["Q_rev_n"], ["negative electrode"]) @@ -108,19 +114,202 @@ def myinterp(t): "Positive electrode potential [V]": comsol_phi_p, "Terminal voltage [V]": pybamm.Function(comsol_voltage, pybamm.t * tau), "Cell temperature [K]": comsol_temperature, + "Volume-averaged cell temperature [K]": pybamm.Function( + comsol_temperature_av, pybamm.t * tau + ), + "Negative electrode irreversible electrochemical heating [W.m-3]": comsol_q_irrev_n, + "Positive electrode irreversible electrochemical heating [W.m-3]": comsol_q_irrev_p, + "Negative electrode reversible heating [W.m-3]": comsol_q_rev_n, + "Positive electrode reversible heating [W.m-3]": comsol_q_rev_p, + "Negative electrode total heating [W.m-3]": comsol_q_total_n, + "Separator total heating [W.m-3]": comsol_q_total_s, + "Positive electrode total heating [W.m-3]": comsol_q_total_p, } "-----------------------------------------------------------------------------" "Plot comparison" -#TODO: fix QuickPlot -for var in comsol_model.variables.keys(): - plot = pybamm.QuickPlot( - [pybamm_model, comsol_model], - mesh, - [solution, solution], - output_variables=[var], - labels=["PyBaMM", "Comsol"], +plot_times = comsol_variables["time"] +pybamm_T = pybamm.ProcessedVariable( + pybamm_model.variables["Volume-averaged cell temperature [K]"], + solution.t, + solution.y, + mesh=mesh, +)(plot_times / tau) +comsol_T = pybamm.ProcessedVariable( + comsol_model.variables["Volume-averaged cell temperature [K]"], + solution.t, + solution.y, + mesh=mesh, +)(plot_times / tau) +plt.figure() +plt.plot(plot_times, pybamm_T, "-", label="PyBaMM") +plt.plot(plot_times, comsol_T, "o", label="COMSOL") +plt.xlabel("t") +plt.ylabel("T") +plt.legend() + +pybamm_voltage = pybamm.ProcessedVariable( + pybamm_model.variables["Terminal voltage [V]"], + solution.t, + solution.y, + mesh=mesh, +)(plot_times / tau) +comsol_voltage = pybamm.ProcessedVariable( + comsol_model.variables["Terminal voltage [V]"], + solution.t, + solution.y, + mesh=mesh, +)(plot_times / tau) +plt.figure() +plt.plot(plot_times, pybamm_voltage, "-", label="PyBaMM") +plt.plot(plot_times, comsol_voltage, "o", label="COMSOL") +plt.xlabel("t") +plt.ylabel("Voltage [V]") +plt.legend() + +# Get mesh nodes +x_n = mesh.combine_submeshes(*["negative electrode"])[0].nodes +x_s = mesh.combine_submeshes(*["separator"])[0].nodes +x_p = mesh.combine_submeshes(*["positive electrode"])[0].nodes +x = mesh.combine_submeshes(*whole_cell)[0].nodes + + +def comparison_plot(var, plot_times=None): + """ + Plot pybamm heat source (defined over whole cell) against comsol heat source + (defined by component) + """ + if plot_times is None: + plot_times = comsol_variables["time"] + + # Process pybamm heat source + pybamm_q = pybamm.ProcessedVariable( + pybamm_model.variables[var], solution.t, solution.y, mesh=mesh + ) + + # Process comsol heat source in negative electrode + comsol_q_n = pybamm.ProcessedVariable( + comsol_model.variables["Negative electrode " + var[0].lower() + var[1:]], + solution.t, + solution.y, + mesh=mesh, + ) + # Process comsol heat source in separator (if defined here) + try: + comsol_q_s = pybamm.ProcessedVariable( + comsol_model.variables["Separator " + var[0].lower() + var[1:]], + solution.t, + solution.y, + mesh=mesh, + ) + except KeyError: + comsol_q_s = None + print("Variable " + var + " not defined in separator") + # Process comsol heat source in positive electrode + comsol_q_p = pybamm.ProcessedVariable( + comsol_model.variables["Positive electrode " + var[0].lower() + var[1:]], + solution.t, + solution.y, + mesh=mesh, ) - plot.dynamic_plot(testing=True) + + # Make plot + if comsol_q_s: + n_cols = 3 + else: + n_cols = 2 + fig, ax = plt.subplots(1, n_cols, figsize=(15, 8)) + cmap = plt.get_cmap("inferno") + + for ind, t in enumerate(plot_times): + color = cmap(float(ind) / len(plot_times)) + ax[0].plot(x_n * L_x, pybamm_q(x=x_n, t=t / tau), "-", color=color) + ax[0].plot(x_n * L_x, comsol_q_n(x=x_n, t=t / tau), "o", color=color) + if comsol_q_s: + ax[1].plot(x_s * L_x, pybamm_q(x=x_s, t=t / tau), "-", color=color) + ax[1].plot(x_s * L_x, comsol_q_s(x=x_s, t=t / tau), "o", color=color) + ax[n_cols - 1].plot( + x_p * L_x, + pybamm_q(x=x_p, t=t / tau), + "-", + color=color, + label="PyBaMM" if ind == 0 else "", + ) + ax[n_cols - 1].plot( + x_p * L_x, + comsol_q_p(x=x_p, t=t / tau), + "o", + color=color, + label="COMSOL" if ind == 0 else "", + ) + + ax[0].set_xlabel("x_n") + ax[0].set_ylabel(var) + if comsol_q_s: + ax[1].set_xlabel("x_s") + ax[1].set_ylabel(var) + ax[n_cols - 1].set_xlabel("x_p") + ax[n_cols - 1].set_ylabel(var) + plt.legend() + plt.tight_layout() + + +def temperature_plot(plot_times=None): + """ + Plot pybamm heat source (defined over whole cell) against comsol heat source + (defined by component) + """ + if plot_times is None: + plot_times = comsol_variables["time"] + + # Process pybamm heat source + pybamm_T = pybamm.ProcessedVariable( + pybamm_model.variables["Cell temperature [K]"], + solution.t, + solution.y, + mesh=mesh, + ) + + # Process comsol heat source in negative electrode + comsol_T = pybamm.ProcessedVariable( + comsol_model.variables["Cell temperature [K]"], + solution.t, + solution.y, + mesh=mesh, + ) + + # Make plot + plt.figure(figsize=(15, 8)) + cmap = plt.get_cmap("inferno") + + for ind, t in enumerate(plot_times): + color = cmap(float(ind) / len(plot_times)) + plt.plot( + x * L_x, + pybamm_T(x=x, t=t / tau), + "-", + color=color, + label="PyBaMM" if ind == 0 else "", + ) + plt.plot( + x * L_x, + comsol_T(x=x, t=t / tau), + "o", + color=color, + label="COMSOL" if ind == 0 else "", + ) + + plt.xlabel("x") + plt.ylabel("Temperature [K]") + plt.legend() + plt.tight_layout() + + +# Make plots +plot_times = comsol_variables["time"][0::10] +comparison_plot("Irreversible electrochemical heating [W.m-3]", plot_times=plot_times) +comparison_plot("Reversible heating [W.m-3]", plot_times=plot_times) +comparison_plot("Total heating [W.m-3]", plot_times=plot_times) +# temperature_plot(plot_times) plt.show() diff --git a/results/comsol_comparison/load_comsol_data.py b/results/comsol_comparison/load_comsol_data.py index ea6f8366e0..a67ab9b8f3 100644 --- a/results/comsol_comparison/load_comsol_data.py +++ b/results/comsol_comparison/load_comsol_data.py @@ -65,6 +65,9 @@ comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" ) +# average temperature +comsol_temp_av = np.mean(comsol_temp_vals, axis=0) + # irreversible heat source in negative electrode comsol = pd.read_csv(path + "q_irrev_n.csv", sep=",", header=None) comsol_q_irrev_n_vals = np.reshape( @@ -125,6 +128,7 @@ "c_p_surf": comsol_c_p_surf_vals, "c_e": comsol_c_e_vals, "temperature": comsol_temp_vals, + "average temperature": comsol_temp_av, "Q_irrev_n": comsol_q_irrev_n_vals, "Q_irrev_p": comsol_q_irrev_p_vals, "Q_rev_n": comsol_q_rev_n_vals, From d6a312fe6b9f2eb9a6000d004b44b31f6294d576 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Tue, 29 Oct 2019 21:58:30 -0400 Subject: [PATCH 065/122] #684 add termination reasons --- examples/scripts/compare_lithium_ion.py | 2 +- pybamm/solvers/casadi_solver.py | 42 ++++++++++++++++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index c61d378686..d92fb46f84 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -47,7 +47,7 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.3, 100) for i, model in enumerate(models): - solutions[i] = pybamm.CasadiSolver().solve(model, t_eval) + solutions[i] = model.default_solver.solve(model, t_eval) # plot plot = pybamm.QuickPlot(models, mesh, solutions) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index c00df6e044..f4df8104c1 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -9,14 +9,29 @@ class CasadiSolver(pybamm.DaeSolver): """Solve a discretised model, using CasADi. + **Extends**: :class:`pybamm.DaeSolver` + Parameters ---------- + method : str, optional + The method to use for solving the system ('cvodes', for ODEs, or 'idas', for + DAEs). Default is 'idas'. rtol : float, optional The relative tolerance for the solver (default is 1e-6). atol : float, optional The absolute tolerance for the solver (default is 1e-6). + root_method : str, optional + The method to use for finding consistend initial conditions. Default is 'lm'. + root_tol : float, optional + The tolerance for root-finding. Default is 1e-6. + max_step_decrease_counts : float, optional + The maximum number of times step size can be decreased before an error is + raised. Default is 10. + extra_options : keyword arguments, optional + Any extra keyword-arguments; these are passed directly to the CasADi integrator. + Please consult `CasADi documentation `_ for + details. - **Extends**: :class:`pybamm.DaeSolver` """ def __init__( @@ -26,11 +41,10 @@ def __init__( atol=1e-6, root_method="lm", root_tol=1e-6, - max_steps=1000, max_step_decrease_count=10, **extra_options, ): - super().__init__(method, rtol, atol, root_method, root_tol, max_steps) + super().__init__(method, rtol, atol, root_method, root_tol) self.max_step_decrease_count = max_step_decrease_count self.extra_options = extra_options self.name = "CasADi solver ({})".format(method) @@ -54,7 +68,7 @@ def solve(self, model, t_eval, mode="safe"): Recommended when simulating a drive cycle or other simulation where \ no events should be triggered. - "safe": perform step-and-check integration, checking whether events have \ - been triggered. Recommended for simulations of a full charge or discharge. + been triggered. Recommended for simulations of a full charge or discharge. Raises ------ @@ -83,6 +97,7 @@ def solve(self, model, t_eval, mode="safe"): pybamm.logger.info( "Start solving {} with {} in 'safe' mode".format(model.name, self.name) ) + self.t = 0.0 for dt in np.diff(t_eval): # Step solved = False @@ -105,7 +120,7 @@ def solve(self, model, t_eval, mode="safe"): t = solution.t raise pybamm.SolverError( """ - Maximum number of decreased steps occurred at t={}. Try + Maximum number of decreased steps occurred at t={}. Try solving the model up to this time only """.format( t @@ -122,6 +137,9 @@ def solve(self, model, t_eval, mode="safe"): ) # Exit loop if the sign of an event changes if (new_event_signs != init_event_signs).any(): + solution.termination = "event" + solution.t_event = solution.t[-1] + solution.y_event = solution.y[:, -1] break else: if not solution: @@ -131,6 +149,14 @@ def solve(self, model, t_eval, mode="safe"): else: # append solution from the current step to solution solution.append(current_step_sol) + if not hasattr(solution, "termination"): + solution.termination = "final time" + + # Calculate more exact termination reason + solution.termination = self.get_termination_reason(solution, self.events) + pybamm.logger.info( + "Finish solving {} ({})".format(model.name, solution.termination) + ) pybamm.logger.info( "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), @@ -142,7 +168,7 @@ def solve(self, model, t_eval, mode="safe"): else: raise ValueError( """ - invalid mode '{}'. Must be either 'safe', for solving with events, + invalid mode '{}'. Must be either 'safe', for solving with events, or 'fast', for solving quickly without events""".format( mode ) @@ -171,8 +197,8 @@ def compute_solution(self, model, t_eval): ) solve_time = timer.time() - solve_start_time - # Identify the event that caused termination - termination = self.get_termination_reason(solution, self.events) + # Events not implemented, termination is always 'final time' + termination = "final time" return solution, solve_time, termination From 7da7d163ddf17cb73145df0f527a3dc51924acd0 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Tue, 29 Oct 2019 22:00:59 -0400 Subject: [PATCH 066/122] #684 casadi --- pybamm/solvers/casadi_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index f4df8104c1..9e2f094d4b 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -68,7 +68,7 @@ def solve(self, model, t_eval, mode="safe"): Recommended when simulating a drive cycle or other simulation where \ no events should be triggered. - "safe": perform step-and-check integration, checking whether events have \ - been triggered. Recommended for simulations of a full charge or discharge. + been triggered. Recommended for simulations of a full charge or discharge. Raises ------ From 2750870fa767d0cb5cfab5a744aeeabd10bdabb6 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 10:24:40 +0000 Subject: [PATCH 067/122] #678 adding missing electrolyte temp dependence --- .../base_electrolyte_conductivity.py | 2 +- ...igher_order_stefan_maxwell_conductivity.py | 33 +++++++++++-------- .../full_stefan_maxwell_conductivity.py | 3 +- ...urface_form_stefan_maxwell_conductivity.py | 16 ++++++--- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py index cb3c49e64f..b282465c7b 100644 --- a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py +++ b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py @@ -258,7 +258,7 @@ def _get_domain_current_variables(self, i_e, domain=None): def _get_whole_cell_variables(self, variables): """ A private function to obtain the potential and current concatenated - across the whole cell. Note required 'variables' to contain the potential + across the whole cell. Note: requires 'variables' to contain the potential and current in the subdomains: 'negative electrode', 'separator', and 'positive electrode'. diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py index 0b482ba956..b6c5bf756c 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py @@ -48,11 +48,10 @@ def get_coupled_variables(self, variables): eps_s_av = variables["Leading-order x-averaged separator porosity"] eps_p_av = variables["Leading-order x-averaged positive electrode porosity"] - # Note: here we want the average of the temperature over the negative - # electrode, separator and positive electrode (not including the current - # collectors) - T = variables["Cell temperature"] - T_av = pybamm.x_average(T) + T_av = variables["X-averaged cell temperature"] + T_av_n = pybamm.PrimaryBroadcast(T_av, "negative electrode") + T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") + T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") c_e_n, c_e_s, c_e_p = c_e.orphans @@ -90,6 +89,7 @@ def get_coupled_variables(self, variables): + phi_s_n_av - ( chi_av + * (1 + param.Theta * T_av) * pybamm.x_average( self._higher_order_macinnes_function( c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") @@ -106,6 +106,7 @@ def get_coupled_variables(self, variables): pybamm.PrimaryBroadcast(phi_e_const, "negative electrode") + ( chi_av_n + * (1 + param.Theta * T_av_n) * self._higher_order_macinnes_function( c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") ) @@ -124,6 +125,7 @@ def get_coupled_variables(self, variables): pybamm.PrimaryBroadcast(phi_e_const, "separator") + ( chi_av_s + * (1 + param.Theta * T_av_s) * self._higher_order_macinnes_function( c_e_s / pybamm.PrimaryBroadcast(c_e_av, "separator") ) @@ -137,6 +139,7 @@ def get_coupled_variables(self, variables): pybamm.PrimaryBroadcast(phi_e_const, "positive electrode") + ( chi_av_p + * (1 + param.Theta * T_av_p) * self._higher_order_macinnes_function( c_e_p / pybamm.PrimaryBroadcast(c_e_av, "positive electrode") ) @@ -155,15 +158,19 @@ def get_coupled_variables(self, variables): phi_e_av = pybamm.x_average(phi_e) # concentration overpotential - eta_c_av = chi_av * ( - pybamm.x_average( - self._higher_order_macinnes_function( - c_e_p / pybamm.PrimaryBroadcast(c_e_av, "positive electrode") + eta_c_av = ( + chi_av + * (1 + param.Theta * T_av) + * ( + pybamm.x_average( + self._higher_order_macinnes_function( + c_e_p / pybamm.PrimaryBroadcast(c_e_av, "positive electrode") + ) ) - ) - - pybamm.x_average( - self._higher_order_macinnes_function( - c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") + - pybamm.x_average( + self._higher_order_macinnes_function( + c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") + ) ) ) ) diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py index 536c989094..f4fae2f790 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py @@ -39,7 +39,8 @@ def get_coupled_variables(self, variables): phi_e = variables["Electrolyte potential"] i_e = (param.kappa_e(c_e, T) * (eps ** param.b) * param.gamma_e / param.C_e) * ( - param.chi(c_e) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e) + param.chi(c_e) * (1 + param.Theta * T) * pybamm.grad(c_e) / c_e + - pybamm.grad(phi_e) ) variables.update(self._get_standard_current_variables(i_e)) diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py index 5059986287..0c49892b9a 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py @@ -73,13 +73,17 @@ def set_boundary_conditions(self, variables): i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[self.domain + " electrode surface potential difference"] + T = variables["Cell temperature"] if self.domain == "Negative": c_e_flux = pybamm.BoundaryGradient(c_e, "right") flux_left = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") flux_right = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "right")) - - pybamm.BoundaryValue(param.chi(c_e) / c_e, "right") * c_e_flux + - pybamm.BoundaryValue( + (1 + param.Theta * T) * param.chi(c_e) / c_e, "right" + ) + * c_e_flux - i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "right") ) @@ -92,7 +96,10 @@ def set_boundary_conditions(self, variables): c_e_flux = pybamm.BoundaryGradient(c_e, "left") flux_left = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "left")) - - pybamm.BoundaryValue(param.chi(c_e) / c_e, "left") * c_e_flux + - pybamm.BoundaryValue( + (1 + param.Theta * T) * param.chi(c_e) / c_e, "left" + ) + * c_e_flux - i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") ) flux_right = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "right") @@ -152,9 +159,10 @@ def _get_neg_pos_coupled_variables(self, variables): i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[self.domain + " electrode surface potential difference"] + T = variables[self.domain + " temperature"] i_e = conductivity * ( - (param.chi(c_e) / c_e) * pybamm.grad(c_e) + ((1 + param.Theta * T) * param.chi(c_e) / c_e) * pybamm.grad(c_e) + pybamm.grad(delta_phi) + pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) / sigma_eff @@ -190,7 +198,7 @@ def _get_sep_coupled_variables(self, variables): phi_e_s = pybamm.PrimaryBroadcast( pybamm.boundary_value(phi_e_n, "right"), "separator" ) + pybamm.IndefiniteIntegral( - chi_e_s / c_e_s * pybamm.grad(c_e_s) + (1 + param.Theta * T) * chi_e_s / c_e_s * pybamm.grad(c_e_s) - param.C_e * pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) / kappa_s_eff, From 57c73c7a09ae976a273d6222a12184d0f0084e2b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 11:11:19 +0000 Subject: [PATCH 068/122] #678 debug electrolyte --- .../full_surface_form_stefan_maxwell_conductivity.py | 5 +++-- pybamm/models/submodels/thermal/base_thermal.py | 4 ++-- pybamm/models/submodels/thermal/isothermal/isothermal.py | 4 ++-- .../test_composite_stefan_maxwell_conductivity.py | 2 +- .../test_full_surface_form_stefan_maxwell_conductivity.py | 2 ++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py index 0c49892b9a..996f9fdfe3 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py @@ -73,9 +73,9 @@ def set_boundary_conditions(self, variables): i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[self.domain + " electrode surface potential difference"] - T = variables["Cell temperature"] if self.domain == "Negative": + T = variables["Negative electrode temperature"] c_e_flux = pybamm.BoundaryGradient(c_e, "right") flux_left = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") flux_right = ( @@ -93,6 +93,7 @@ def set_boundary_conditions(self, variables): rbc_c_e = (c_e_flux, "Neumann") elif self.domain == "Positive": + T = variables["Positive electrode temperature"] c_e_flux = pybamm.BoundaryGradient(c_e, "left") flux_left = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "left")) @@ -159,7 +160,7 @@ def _get_neg_pos_coupled_variables(self, variables): i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[self.domain + " electrode surface potential difference"] - T = variables[self.domain + " temperature"] + T = variables[self.domain + " electrode temperature"] i_e = conductivity * ( ((1 + param.Theta * T) * param.chi(c_e) / c_e) * pybamm.grad(c_e) diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index 4495694c82..f2b16de228 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -179,8 +179,8 @@ def _get_standard_coupled_variables(self, variables): "X-averaged irreversible electrochemical heating [W.m-3]": Q_rxn_av * Q_scale, "Volume-averaged irreversible electrochemical heating": Q_rxn_vol_av, - "Volume-averaged irreversible electrochemical heating [W.m-3]": Q_rxn_vol_av - * Q_scale, + "Volume-averaged irreversible electrochemical heating [W.m-3]": + Q_rxn_vol_av * Q_scale, "Reversible heating": Q_rev, "Reversible heating [W.m-3]": Q_rev * Q_scale, "X-averaged reversible heating": Q_rev_av, diff --git a/pybamm/models/submodels/thermal/isothermal/isothermal.py b/pybamm/models/submodels/thermal/isothermal/isothermal.py index f8ecdfc5ab..3c12d58d5e 100644 --- a/pybamm/models/submodels/thermal/isothermal/isothermal.py +++ b/pybamm/models/submodels/thermal/isothermal/isothermal.py @@ -73,6 +73,6 @@ def _yz_average(self, var): def _x_average(self, var, var_cn, var_cp): """ Temperature is uniform and heat source terms are zero, so the average - returns the input variable. + returns zeros broadcasted onto the current collector domain. """ - return var + return pybamm.PrimaryBroadcast(0, "current collector") diff --git a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py index 6eff2e9d01..c3e55d659f 100644 --- a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py +++ b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py @@ -22,7 +22,7 @@ def test_public_functions(self): "Leading-order x-averaged negative electrode porosity": a, "Leading-order x-averaged separator porosity": a, "Leading-order x-averaged positive electrode porosity": a, - "Cell temperature": a, + "X-averaged cell temperature": a, } submodel = pybamm.electrolyte.stefan_maxwell.conductivity.Composite(param) std_tests = tests.StandardSubModelTests(submodel, variables) diff --git a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py index 464cb91915..bc4bc47d73 100644 --- a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py +++ b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py @@ -27,6 +27,8 @@ def test_public_functions(self): "Negative electrode interfacial current density": a_n, "Electrolyte potential": pybamm.Concatenation(a_n, a_s, a_p), "Negative electrode temperature": a_n, + "Separator temperature": a_s, + "Positive electrode temperature": a_p, } icd = " interfacial current density" reactions = { From 9940bf76a6a3a0daad95ef114243c02b575fe71d Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 14:45:10 +0000 Subject: [PATCH 069/122] #678 fix temp depend kinetics --- .../inverse_kinetics/base_inverse_kinetics.py | 9 +- .../inverse_kinetics/inverse_butler_volmer.py | 6 +- .../interface/kinetics/base_kinetics.py | 25 ++- .../interface/kinetics/butler_volmer.py | 19 ++- .../interface/kinetics/no_reaction.py | 2 +- .../submodels/interface/kinetics/tafel.py | 23 ++- .../models/submodels/thermal/base_thermal.py | 147 ------------------ .../xyz_lumped_2D_current_collector.py | 2 +- .../test_interface/test_lead_acid.py | 2 + 9 files changed, 59 insertions(+), 176 deletions(-) diff --git a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py b/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py index 6c7acfe61c..244f3d2d65 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py @@ -41,8 +41,13 @@ def get_coupled_variables(self, variables): ne = self.param.ne_n elif self.domain == "Positive": ne = self.param.ne_p + # Note: T must have the same domain as j0 and eta_r + if j0.domain in ["current collector", ["current collector"]]: + T = variables["X-averaged cell temperature"] + else: + T = variables[self.domain + " electrode temperature"] - eta_r = self._get_overpotential(j, j0, ne) + eta_r = self._get_overpotential(j, j0, ne, T) delta_phi = eta_r + ocp variables.update(self._get_standard_interfacial_current_variables(j)) @@ -73,5 +78,5 @@ def get_coupled_variables(self, variables): return variables - def _get_overpotential(self, j, j0, ne): + def _get_overpotential(self, j, j0, ne, T): raise NotImplementedError diff --git a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py index 37afd228ab..7edd3b5753 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -28,5 +28,7 @@ class InverseButlerVolmer(BaseInverseKinetics, ButlerVolmer): def __init__(self, param, domain): super().__init__(param, domain) - def _get_overpotential(self, j, j0, ne): - return (2 / ne) * pybamm.Function(np.arcsinh, j / (2 * j0)) + def _get_overpotential(self, j, j0, ne, T): + return (2 * (1 + self.param.Theta * T) / ne) * pybamm.Function( + np.arcsinh, j / (2 * j0) + ) diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index a4cfe9f33e..b65181e792 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -40,8 +40,13 @@ def get_coupled_variables(self, variables): eta_r = delta_phi - ocp # Get number of electrons in reaction ne = self._get_number_of_electrons_in_reaction() - - j = self._get_kinetics(j0, ne, eta_r) + # Get kinetics. Note: T must have the same domain as j0 and eta_r + if j0.domain in ["current collector", ["current collector"]]: + T = variables["X-averaged cell temperature"] + else: + T = variables[self.domain + " electrode temperature"] + j = self._get_kinetics(j0, ne, eta_r, T) + # Get average interfacial current density j_tot_av = self._get_average_total_interfacial_current_density(variables) # j = j_tot_av + (j - pybamm.x_average(j)) # enforce true average @@ -73,7 +78,7 @@ def get_coupled_variables(self, variables): def _get_exchange_current_density(self, variables): raise NotImplementedError - def _get_kinetics(self, j0, ne, eta_r): + def _get_kinetics(self, j0, ne, eta_r, T): raise NotImplementedError def _get_open_circuit_potential(self, variables): @@ -84,10 +89,10 @@ def _get_dj_dc(self, variables): Default to calculate derivative of interfacial current density with respect to concentration. Can be overwritten by specific kinetic functions. """ - c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) - j = self._get_kinetics(j0, ne, delta_phi - ocp) + j = self._get_kinetics(j0, ne, delta_phi - ocp, T) return j.diff(c_e) def _get_dj_ddeltaphi(self, variables): @@ -95,10 +100,10 @@ def _get_dj_ddeltaphi(self, variables): Default to calculate derivative of interfacial current density with respect to surface potential difference. Can be overwritten by specific kinetic functions. """ - _, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) - j = self._get_kinetics(j0, ne, delta_phi - ocp) + j = self._get_kinetics(j0, ne, delta_phi - ocp, T) return j.diff(delta_phi) def _get_interface_variables_for_first_order(self, variables): @@ -118,7 +123,11 @@ def _get_interface_variables_for_first_order(self, variables): j0 = self._get_exchange_current_density(hacked_variables) ne = self._get_number_of_electrons_in_reaction() ocp = self._get_open_circuit_potential(hacked_variables)[0] - return c_e_0, delta_phi, j0, ne, ocp + if j0.domain in ["current collector", ["current collector"]]: + T = variables["X-averaged cell temperature"] + else: + T = variables[self.domain + " electrode temperature"] + return c_e_0, delta_phi, j0, ne, ocp, T def _get_j_diffusion_limited_first_order(self, variables): """ diff --git a/pybamm/models/submodels/interface/kinetics/butler_volmer.py b/pybamm/models/submodels/interface/kinetics/butler_volmer.py index 437298bb84..0ee18a8f6b 100644 --- a/pybamm/models/submodels/interface/kinetics/butler_volmer.py +++ b/pybamm/models/submodels/interface/kinetics/butler_volmer.py @@ -12,7 +12,7 @@ class ButlerVolmer(BaseModel): Base submodel which implements the forward Butler-Volmer equation: .. math:: - j = j_0(c) * \\sinh(\\eta_r(c)) + j = 2 * j_0(c) * \\sinh( (ne / (2 * (1 + \\Theta T)) * \\eta_r(c)) Parameters ---------- @@ -28,26 +28,29 @@ class ButlerVolmer(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): - return 2 * j0 * pybamm.sinh((ne / 2) * eta_r) + def _get_kinetics(self, j0, ne, eta_r, T): + prefactor = ne / (2 * (1 + self.param.Theta * T)) + return 2 * j0 * pybamm.sinh(prefactor * eta_r) def _get_dj_dc(self, variables): "See :meth:`pybamm.interface.kinetics.BaseModel._get_dj_dc`" - c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return (2 * j0.diff(c_e) * pybamm.sinh((ne / 2) * eta_r)) - ( - 2 * j0 * (ne / 2) * ocp.diff(c_e) * pybamm.cosh((ne / 2) * eta_r) + prefactor = ne / (2 * (1 + self.param.Theta * T)) + return (2 * j0.diff(c_e) * pybamm.sinh(prefactor * eta_r)) - ( + 2 * j0 * prefactor * ocp.diff(c_e) * pybamm.cosh(prefactor * eta_r) ) def _get_dj_ddeltaphi(self, variables): "See :meth:`pybamm.interface.kinetics.BaseModel._get_dj_ddeltaphi`" - _, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return 2 * j0 * (ne / 2) * pybamm.cosh((ne / 2) * eta_r) + prefactor = ne / (2 * (1 + self.param.Theta * T)) + return 2 * j0 * prefactor * pybamm.cosh(prefactor * eta_r) class FirstOrderButlerVolmer(ButlerVolmer, BaseFirstOrderKinetics): diff --git a/pybamm/models/submodels/interface/kinetics/no_reaction.py b/pybamm/models/submodels/interface/kinetics/no_reaction.py index e135c5f25c..d16c32a933 100644 --- a/pybamm/models/submodels/interface/kinetics/no_reaction.py +++ b/pybamm/models/submodels/interface/kinetics/no_reaction.py @@ -24,5 +24,5 @@ class NoReaction(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): + def _get_kinetics(self, j0, ne, eta_r, T): return pybamm.Scalar(0) diff --git a/pybamm/models/submodels/interface/kinetics/tafel.py b/pybamm/models/submodels/interface/kinetics/tafel.py index fbd0c5f770..a263657353 100644 --- a/pybamm/models/submodels/interface/kinetics/tafel.py +++ b/pybamm/models/submodels/interface/kinetics/tafel.py @@ -11,7 +11,7 @@ class ForwardTafel(BaseModel): Base submodel which implements the forward Tafel equation: .. math:: - j = j_0(c) * \\exp(\\eta_r(c)) + j = j_0(c) * \\exp((ne / (2 * (1 + \\Theta T)) * \\eta_r(c)) Parameters ---------- @@ -27,24 +27,33 @@ class ForwardTafel(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): - return j0 * pybamm.exp((ne / 2) * eta_r) + def _get_kinetics(self, j0, ne, eta_r, T): + return j0 * pybamm.exp((ne / (2 * (1 + self.param.Theta * T))) * eta_r) def _get_dj_dc(self, variables): "See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_dc`" - c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return 2 * j0.diff(c_e) * pybamm.exp((ne / 2) * eta_r) + return ( + 2 + * j0.diff(c_e) + * pybamm.exp((ne / (2 * (1 + self.param.Theta * T))) * eta_r) + ) def _get_dj_ddeltaphi(self, variables): "See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_ddeltaphi`" - _, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return 2 * j0 * (ne / 2) * pybamm.exp((ne / 2) * eta_r) + return ( + 2 + * j0 + * (ne / (2 * (1 + self.param.Theta * T))) + * pybamm.exp((ne / 2) * eta_r) + ) class FirstOrderForwardTafel(ForwardTafel, BaseFirstOrderKinetics): diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index f2b16de228..2f7186b187 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -159,12 +159,6 @@ def _get_standard_coupled_variables(self, variables): # Dimensional scaling for heat source terms Q_scale = param.i_typ * param.potential_scale / param.L_x - # Geat heat source by domain - if isinstance(Q_ohm_e, pybamm.Concatenation): - variables.update( - self._get_domain_heat_source_terms(Q_ohm_s, Q_ohm_e, Q_rxn, Q_rev) - ) - variables.update( { "Ohmic heating": Q_ohm, @@ -197,147 +191,6 @@ def _get_standard_coupled_variables(self, variables): ) return variables - def _get_domain_heat_source_terms(self, Q_ohm_s, Q_ohm_e, Q_rxn, Q_rev): - """ - A private function to obtain the heat source terms split - by domain: 'negative electrode', 'separator' and 'positive electrode'. - - Parameters - ---------- - Q_ohm_s : :class:`pybamm.Symbol` - The Ohmic heating in the solid. - Q_ohm_e : :class:`pybamm.Symbol` - The Ohmic heating in the electrolyte. - Q_rxn : :class:`pybamm.Symbol` - The irreversible electrochemical heating. - Q_ohm_s : :class:`pybamm.Symbol` - The reversible electrochemical heating. - - Returns - ------- - variables : dict - The variables corresponding to the heat source terms in the - separate domains. - """ - param = self.param - - # Unpack by domain - Q_ohm_s_n, _, Q_ohm_s_p = Q_ohm_s.orphans - Q_ohm_e_n, Q_ohm_e_s, Q_ohm_e_p = Q_ohm_e.orphans - Q_ohm_n = Q_ohm_s_n + Q_ohm_e_n - Q_ohm_p = Q_ohm_s_p + Q_ohm_e_p - Q_rxn_n, _, Q_rxn_p = Q_rxn.orphans - Q_rev_n, _, Q_rev_p = Q_rev.orphans - Q_n = Q_ohm_n + Q_rxn_n + Q_rev_n - Q_s = Q_ohm_e_s - Q_p = Q_ohm_p + Q_rxn_p + Q_rev_p - - # X-average - Q_ohm_n_av = pybamm.x_average(Q_ohm_n) - Q_ohm_s_av = pybamm.x_average(Q_ohm_e_s) - Q_ohm_p_av = pybamm.x_average(Q_ohm_p) - Q_rxn_n_av = pybamm.x_average(Q_rxn_n) - Q_rxn_p_av = pybamm.x_average(Q_rxn_n) - Q_rev_n_av = pybamm.x_average(Q_rev_n) - Q_rev_p_av = pybamm.x_average(Q_rev_n) - Q_n_av = pybamm.x_average(Q_n) - Q_s_av = pybamm.x_average(Q_s) - Q_p_av = pybamm.x_average(Q_p) - - # Volume average - Q_ohm_n_vol_av = self._yz_average(Q_ohm_n_av) - Q_ohm_s_vol_av = self._yz_average(Q_ohm_s_av) - Q_ohm_p_vol_av = self._yz_average(Q_ohm_p_av) - Q_rxn_n_vol_av = self._yz_average(Q_rxn_n_av) - Q_rxn_p_vol_av = self._yz_average(Q_rxn_p_av) - Q_rev_n_vol_av = self._yz_average(Q_rev_n_av) - Q_rev_p_vol_av = self._yz_average(Q_rev_p_av) - Q_n_vol_av = self._yz_average(Q_n_av) - Q_s_vol_av = self._yz_average(Q_s_av) - Q_p_vol_av = self._yz_average(Q_p_av) - - # Dimensional scaling for heat source terms - Q_scale = param.i_typ * param.potential_scale / param.L_x - - variables = { - "Negative electrode Ohmic heating": Q_ohm_n, - "Negative electrode Ohmic heating [W.m-3]": Q_ohm_n * Q_scale, - "X-averaged negative electrode Ohmic heating": Q_ohm_n_av, - "X-averaged negative electrode Ohmic heating [W.m-3]": Q_ohm_n_av * Q_scale, - "Volume-averaged negative electrode ohm heating": Q_ohm_n_vol_av, - "Volume-averaged negative electrode ohm heating [W.m-3]": Q_ohm_n_vol_av - * Q_scale, - "Separator Ohmic heating": Q_ohm_s, - "Separator Ohmic heating [W.m-3]": Q_ohm_s * Q_scale, - "X-averaged separator Ohmic heating": Q_ohm_s_av, - "X-averaged separator Ohmic heating [W.m-3]": Q_ohm_s_av * Q_scale, - "Volume-averaged separator ohm heating": Q_ohm_s_vol_av, - "Volume-averaged separator ohm heating [W.m-3]": Q_ohm_s_vol_av * Q_scale, - "Positive electrode Ohmic heating": Q_ohm_p, - "Positive electrode Ohmic heating [W.m-3]": Q_ohm_p * Q_scale, - "X-averaged positive electrode Ohmic heating": Q_ohm_p_av, - "X-averaged positive electrode Ohmic heating [W.m-3]": Q_ohm_p_av * Q_scale, - "Volume-averaged positive electrode Ohmic heating": Q_ohm_p_vol_av, - "Volume-averaged positive electrode Ohmic heating [W.m-3]": Q_ohm_p_vol_av - * Q_scale, - "Negative electrode irreversible electrochemical heating": Q_rxn_n_av, - "Negative electrode irreversible electrochemical heating [W.m-3]": Q_rxn_n_av - * Q_scale, - "X-averaged negative electrode irreversible electrochemical heating": Q_rxn_n_av, - "X-averaged negative electrode irreversible electrochemical heating [W.m-3]": Q_rxn_n_av - * Q_scale, - "Volume-averaged negative electrode irreversible electrochemical heating": Q_rxn_n_vol_av, - "Volume-averaged negative electrode irreversible electrochemical heating [W.m-3]": Q_rxn_n_vol_av - * Q_scale, - "Positive electrode irreversible electrochemical heating": Q_rxn_p_av, - "Positive electrode irreversible electrochemical heating [W.m-3]": Q_rxn_p_av - * Q_scale, - "X-averaged positive electrode irreversible electrochemical heating": Q_rxn_p_av, - "X-averaged positive electrode irreversible electrochemical heating [W.m-3]": Q_rxn_p_av - * Q_scale, - "Volume-averaged positive electrode irreversible electrochemical heating": Q_rxn_p_vol_av, - "Volume-averaged positive electrode irreversible electrochemical heating [W.m-3]": Q_rxn_p_vol_av - * Q_scale, - "Negative electrode reversible heating": Q_rev_n, - "Negative electrode reversible heating [W.m-3]": Q_rev_n * Q_scale, - "X-averaged negative electrode reversible heating": Q_rev_n_av, - "X-averaged negative electrode reversible heating [W.m-3]": Q_rev_n_av - * Q_scale, - "Volume-averaged negative electrode reversible heating": Q_rev_n_vol_av, - "Volume-averaged negative electrode reversible heating [W.m-3]": Q_rev_n_vol_av - * Q_scale, - "Positive electrode reversible heating": Q_rev_p, - "Positive electrode reversible heating [W.m-3]": Q_rev_p * Q_scale, - "X-averaged positive electrode reversible heating": Q_rev_p_av, - "X-averaged positive electrode reversible heating [W.m-3]": Q_rev_p_av - * Q_scale, - "Volume-averaged positive electrode reversible heating": Q_rev_p_vol_av, - "Volume-averaged positive electrode reversible heating [W.m-3]": Q_rev_p_vol_av - * Q_scale, - "Negative electrode total heating": Q_n, - "Negative electrode total heating [W.m-3]": Q_n * Q_scale, - "X-averaged negative electrode total heating": Q_n_av, - "X-averaged negative electrode total heating [W.m-3]": Q_n_av * Q_scale, - "Volume-averaged negative electrode total heating": Q_n_vol_av, - "Volume-averaged negative electrode total heating [W.m-3]": Q_n_vol_av - * Q_scale, - "Separator total heating": Q_s, - "Separator total heating [W.m-3]": Q_s * Q_scale, - "X-averaged separator total heating": Q_s_av, - "X-averaged separator total heating [W.m-3]": Q_s_av * Q_scale, - "Volume-averaged separator total heating": Q_s_vol_av, - "Volume-averaged separator total heating [W.m-3]": Q_s_vol_av * Q_scale, - "Positive electrode total heating": Q_p, - "Positive electrode total heating [W.m-3]": Q_p * Q_scale, - "X-averaged postive electrode total heating": Q_p_av, - "X-averaged postive electrode total heating [W.m-3]": Q_p_av * Q_scale, - "Volume-averaged postive electrode total heating": Q_p_vol_av, - "Volume-averaged positive electrode total heating [W.m-3]": Q_p_vol_av - * Q_scale, - } - - return variables - def _flux_law(self, T): raise NotImplementedError diff --git a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py index bf2f9f3c5e..60fe551a74 100644 --- a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py +++ b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py @@ -33,7 +33,7 @@ def _current_collector_heating(self, variables): def _surface_cooling_coefficient(self): """Returns the surface cooling coefficient in 2+1D""" - # Note: assumes pouch cell geometry + # Note: assumes pouch cell geometry return ( -2 * self.param.h / (self.param.delta ** 2) / self.param.l - 2 * (self.param.l_y + self.param.l_z) * self.param.h / self.param.delta diff --git a/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py b/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py index bad1837147..da98c47ac0 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py @@ -20,6 +20,7 @@ def test_public_functions(self): "Negative electrolyte potential": a_n, "Negative electrode open circuit potential": a_n, "Negative electrolyte concentration": a_n, + "Negative electrode temperature": a_n, } submodel = pybamm.interface.lead_acid.ButlerVolmer(param, "Negative") std_tests = tests.StandardSubModelTests(submodel, variables) @@ -32,6 +33,7 @@ def test_public_functions(self): "Positive electrolyte potential": a_p, "Positive electrode open circuit potential": a_p, "Positive electrolyte concentration": a_p, + "Positive electrode temperature": a_p, "Negative electrode interfacial current density": a_n, "Negative electrode exchange current density": a_n, } From 7cd0afcbee6f20ae690fdb05ec94c9f8b7a0034b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 15:41:10 +0000 Subject: [PATCH 070/122] #548 tino comments --- CHANGELOG.md | 3 +- .../set_potential_single_particle.rst | 6 + .../full_battery_models/base_battery_model.py | 10 +- .../submodels/current_collector/__init__.py | 6 +- .../current_collector/potential_pair.py | 2 +- .../set_potential_single_particle.py | 29 +++- .../2plus1D/set_temperature_spm_1plus1D.py | 139 +++++++----------- results/2plus1D/spm_1plus1D.py | 2 +- .../test_base_battery_model.py | 6 + .../test_lithium_ion/test_spm.py | 4 + .../test_set_potential_spm_1plus1d.py | 19 +++ 11 files changed, 130 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b9c20626..0c656afe1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added interface (via pybind11) to sundials with the IDA KLU sparse linear solver ([#657](https://github.com/pybamm-team/PyBaMM/pull/657)) - Allow parameters to be set by material or by specifying a particular paper ([#647](https://github.com/pybamm-team/PyBaMM/pull/647)) - Set relative and absolute tolerances independently in solvers ([#645](https://github.com/pybamm-team/PyBaMM/pull/645)) +- Add basic method to allow (a part of) the State Vector to be updated with results obtained from another solution or package ([#624](https://github.com/pybamm-team/PyBaMM/pull/624)) - Add some non-uniform meshes in 1D and 2D ([#617](https://github.com/pybamm-team/PyBaMM/pull/617)) ## Optimizations @@ -20,7 +21,7 @@ ## Bug fixes - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) -- Add warning if `ProcessedVariable` is called outisde its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) +- Add warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) - Improve the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) # [v0.1.0](https://github.com/pybamm-team/PyBaMM/tree/v0.1.0) - 2019-10-08 diff --git a/docs/source/models/submodels/current_collector/set_potential_single_particle.rst b/docs/source/models/submodels/current_collector/set_potential_single_particle.rst index c96067f274..b87ebbf42e 100644 --- a/docs/source/models/submodels/current_collector/set_potential_single_particle.rst +++ b/docs/source/models/submodels/current_collector/set_potential_single_particle.rst @@ -1,5 +1,11 @@ Set Potential Single Particle Models ==================================== +.. autoclass:: pybamm.current_collector.BaseSetPotentialSingleParticle + :members: + .. autoclass:: pybamm.current_collector.SetPotentialSingleParticle1plus1D :members: + +.. autoclass:: pybamm.current_collector.SetPotentialSingleParticle2plus1D + :members: diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 0a8b01a294..a0767a4573 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -239,7 +239,7 @@ def options(self, extra_options): ) if options[ "current collector" - ] == "single particle potenetial pair" and not isinstance( + ] == "single particle potential pair" and not isinstance( self, (pybamm.lithium_ion.SPM, pybamm.lithium_ion.SPMe) ): raise pybamm.OptionError( @@ -607,9 +607,13 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.SetPotentialSingleParticle1plus1D( self.param ) - elif self.options["dimensionality"] in [0, 2]: + elif self.options["dimensionality"] == 2: + submodel = pybamm.current_collector.SetPotentialSingleParticle2plus1D( + self.param + ) + elif self.options["dimensionality"] == 0: raise NotImplementedError( - """Set potential model only implemented for 1D current + """Set potential model only implemented for 1D or 2D current collectors""" ) self.submodels["current collector"] = submodel diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index a71756c6d6..2b575b750d 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -18,4 +18,8 @@ QuiteConductivePotentialPair1plus1D, QuiteConductivePotentialPair2plus1D, ) -from .set_potential_single_particle import SetPotentialSingleParticle1plus1D +from .set_potential_single_particle import ( + BaseSetPotentialSingleParticle, + SetPotentialSingleParticle1plus1D, + SetPotentialSingleParticle2plus1D, +) diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index 10c1369edb..79b1382a78 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -154,7 +154,7 @@ def set_boundary_conditions(self, variables): # giving the zero Dirichlet condition on phi_s_cn. Elsewhere, the boundary # is insulated, giving no flux conditions on phi_s_cn. This is automatically # applied everywhere, apart from the region corresponding to the projection - # of the positive tab, so we need to explititly apply a zero-flux boundary + # of the positive tab, so we need to explitly apply a zero-flux boundary # condition on the region "positive tab" for phi_s_cn. # A current is drawn from the positive tab, giving the non-zero Neumann # boundary condition on phi_s_cp at "positive tab". Elsewhere, the boundary is diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py index e16fc3dfb3..146094ec6a 100644 --- a/pybamm/models/submodels/current_collector/set_potential_single_particle.py +++ b/pybamm/models/submodels/current_collector/set_potential_single_particle.py @@ -6,8 +6,8 @@ from .base_current_collector import BaseModel -class SetPotentialSingleParticle1plus1D(BaseModel): - """A submodel 1D current collectors which *doesn't* update the potentials +class BaseSetPotentialSingleParticle(BaseModel): + """A submodel for current collectors which *doesn't* update the potentials during solve. This class uses the current-voltage relationship from the SPM(e) (see [1]_) to calculate the current. @@ -86,6 +86,7 @@ def set_initial_conditions(self, variables): param = self.param applied_current = param.current_with_time + cc_area = self._get_effective_current_collector_area() phi_s_cn = variables["Negative current collector potential"] phi_s_cp = variables["Positive current collector potential"] i_boundary_cc = variables["Current collector current density"] @@ -94,5 +95,27 @@ def set_initial_conditions(self, variables): phi_s_cn: pybamm.Scalar(0), phi_s_cp: param.U_p(param.c_p_init, param.T_ref) - param.U_n(param.c_n_init, param.T_ref), - i_boundary_cc: applied_current / param.l_y / param.l_z, + i_boundary_cc: applied_current / cc_area, } + + +class SetPotentialSingleParticle1plus1D(BaseSetPotentialSingleParticle): + "Class for 1+1D set potential model" + + def __init__(self, param): + super().__init__(param) + + def _get_effective_current_collector_area(self): + "In the 1+1D models the current collector effectively has surface area l_z" + return self.param.l_z + + +class SetPotentialSingleParticle2plus1D(BaseSetPotentialSingleParticle): + "Class for 1+1D set potential model" + + def __init__(self, param): + super().__init__(param) + + def _get_effective_current_collector_area(self): + "Return the area of the current collector" + return self.param.l_y * self.param.l_z diff --git a/results/2plus1D/set_temperature_spm_1plus1D.py b/results/2plus1D/set_temperature_spm_1plus1D.py index e10d3a1b93..fca61facbd 100644 --- a/results/2plus1D/set_temperature_spm_1plus1D.py +++ b/results/2plus1D/set_temperature_spm_1plus1D.py @@ -56,46 +56,25 @@ def non_dim_temperature(temperature): return (temperature - T_ref) / Delta_T -# step model in time +# step model in time and process variables for later plotting solver = model.default_solver dt = 0.1 # timestep to take npts = 20 # number of points to store the solution at during this step solution1 = solver.step(model, dt, npts=npts) -phi_s_cn_step1 = pybamm.ProcessedVariable( - model.variables["Negative current collector potential [V]"], - solution1.t, - solution1.y, - mesh=mesh, -) -phi_s_cp_step1 = pybamm.ProcessedVariable( - model.variables["Positive current collector potential [V]"], - solution1.t, - solution1.y, - mesh=mesh, -) -voltage_step1 = pybamm.ProcessedVariable( - model.variables["Terminal voltage [V]"], solution1.t, solution1.y, mesh=mesh -) -current_step1 = pybamm.ProcessedVariable( - model.variables["Current [A]"], solution1.t, solution1.y, mesh=mesh -) -heating_step1 = pybamm.ProcessedVariable( - model.variables["X-averaged total heating [A.V.m-3]"], - solution1.t, - solution1.y, - mesh=mesh, -) -particle_step1 = pybamm.ProcessedVariable( - model.variables["X-averaged positive particle surface concentration [mol.m-3]"], - solution1.t, - solution1.y, - mesh=mesh, -) -temperature_step1 = pybamm.ProcessedVariable( - model.variables["X-averaged cell temperature [K]"], - solution1.t, - solution1.y, - mesh=mesh, +# create dict of variables to post process +output_variables = [ + "Negative current collector potential [V]", + "Positive current collector potential [V]", + "Current [A]", + "X-averaged total heating [A.V.m-3]", + "X-averaged positive particle surface concentration [mol.m-3]", + "X-averaged cell temperature [K]", +] +output_variables_dict = {} +for var in output_variables: + output_variables_dict[var] = model.variables[var] +processed_vars_step1 = pybamm.post_process_variables( + output_variables_dict, solution1.t, solution1.y, mesh ) # get the current state and temperature @@ -109,7 +88,7 @@ def non_dim_temperature(temperature): variables = {"X-averaged cell temperature": non_dim_t_external} new_state = update_statevector(variables, current_state) -# step in time again +# step in time again and process variables for later plotting # use new state as initial condition. Note: need to to recompute consistent initial # values for the algebraic part of the model. Since the (dummy) equation for the # temperature is an ODE, the imposed change in temperature is unaffected by this @@ -118,47 +97,12 @@ def non_dim_temperature(temperature): solver.rhs, solver.algebraic, new_state ) solution2 = solver.step(model, dt, npts=npts) -phi_s_cn_step2 = pybamm.ProcessedVariable( - model.variables["Negative current collector potential [V]"], - solution2.t, - solution2.y, - mesh=mesh, -) -phi_s_cp_step2 = pybamm.ProcessedVariable( - model.variables["Positive current collector potential [V]"], - solution2.t, - solution2.y, - mesh=mesh, -) -voltage_step2 = pybamm.ProcessedVariable( - model.variables["Terminal voltage [V]"], solution2.t, solution2.y, mesh=mesh -) -current_step2 = pybamm.ProcessedVariable( - model.variables["Current [A]"], solution2.t, solution2.y, mesh=mesh -) -heating_step2 = pybamm.ProcessedVariable( - model.variables["X-averaged total heating [A.V.m-3]"], - solution2.t, - solution2.y, - mesh=mesh, -) -particle_step2 = pybamm.ProcessedVariable( - model.variables["X-averaged positive particle surface concentration [mol.m-3]"], - solution2.t, - solution2.y, - mesh=mesh, -) -temperature_step2 = pybamm.ProcessedVariable( - model.variables["X-averaged cell temperature [K]"], - solution2.t, - solution2.y, - mesh=mesh, +processed_vars_step2 = pybamm.post_process_variables( + output_variables_dict, solution2.t, solution2.y, mesh ) # plots -t_sec = param.process_symbol( - pybamm.standard_parameters_lithium_ion.tau_discharge -).evaluate() +t_sec = param.evaluate(pybamm.standard_parameters_lithium_ion.tau_discharge) t_hour = t_sec / (3600) z = np.linspace(0, 1, nbat) @@ -167,11 +111,19 @@ def non_dim_temperature(temperature): for bat_id in range(nbat): plt.plot( solution1.t * t_hour, - phi_s_cp_step1(solution1.t, z=z)[bat_id, :] - - phi_s_cn_step1(solution1.t, z=z)[bat_id, :], + processed_vars_step1["Positive current collector potential [V]"]( + solution1.t, z=z + )[bat_id, :] + - processed_vars_step1["Negative current collector potential [V]"]( + solution1.t, z=z + )[bat_id, :], solution2.t * t_hour, - phi_s_cp_step2(solution2.t, z=z)[bat_id, :] - - phi_s_cn_step2(solution2.t, z=z)[bat_id, :], + processed_vars_step2["Positive current collector potential [V]"]( + solution2.t, z=z + )[bat_id, :] + - processed_vars_step1["Negative current collector potential [V]"]( + solution2.t, z=z + )[bat_id, :], ) plt.xlabel("t [hrs]") plt.ylabel("Local voltage [V]") @@ -179,7 +131,10 @@ def non_dim_temperature(temperature): # applied current plt.figure() plt.plot( - solution1.t, current_step1(solution1.t), solution2.t, current_step2(solution2.t) + solution1.t, + processed_vars_step1["Current [A]"](solution1.t), + solution2.t, + processed_vars_step2["Current [A]"](solution2.t), ) plt.xlabel("t") plt.ylabel("Current [A]") @@ -190,9 +145,13 @@ def non_dim_temperature(temperature): for bat_id in range(nbat): plt.plot( solution1.t * t_hour, - heating_step1(solution1.t, z=z)[bat_id, :], + processed_vars_step1["X-averaged total heating [A.V.m-3]"](solution1.t, z=z)[ + bat_id, : + ], solution2.t * t_hour, - heating_step2(solution2.t, z=z)[bat_id, :], + processed_vars_step2["X-averaged total heating [A.V.m-3]"](solution2.t, z=z)[ + bat_id, : + ], ) plt.xlabel("t [hrs]") plt.ylabel("X-averaged total heating [A.V.m-3]") @@ -203,9 +162,13 @@ def non_dim_temperature(temperature): for bat_id in range(nbat): plt.plot( solution1.t * t_hour, - particle_step1(solution1.t, z=z)[bat_id, :], + processed_vars_step1[ + "X-averaged positive particle surface concentration [mol.m-3]" + ](solution1.t, z=z)[bat_id, :], solution2.t * t_hour, - particle_step2(solution2.t, z=z)[bat_id, :], + processed_vars_step2[ + "X-averaged positive particle surface concentration [mol.m-3]" + ](solution2.t, z=z)[bat_id, :], ) plt.xlabel("t [hrs]") plt.ylabel("X-averaged positive particle surface concentration [mol.m-3]") @@ -215,9 +178,13 @@ def non_dim_temperature(temperature): for bat_id in range(nbat): plt.plot( solution1.t * t_hour, - temperature_step1(solution1.t, z=z)[bat_id, :], + processed_vars_step1["X-averaged cell temperature [K]"](solution1.t, z=z)[ + bat_id, : + ], solution2.t * t_hour, - temperature_step2(solution2.t, z=z)[bat_id, :], + processed_vars_step2["X-averaged cell temperature [K]"](solution2.t, z=z)[ + bat_id, : + ], ) plt.xlabel("t [hrs]") plt.ylabel("X-averaged cell temperature [K]") diff --git a/results/2plus1D/spm_1plus1D.py b/results/2plus1D/spm_1plus1D.py index 8665d93767..993ab68ae9 100644 --- a/results/2plus1D/spm_1plus1D.py +++ b/results/2plus1D/spm_1plus1D.py @@ -53,7 +53,7 @@ output_variables = [ "X-averaged negative particle surface concentration [mol.m-3]", "X-averaged positive particle surface concentration [mol.m-3]", - #"X-averaged cell temperature [K]", + # "X-averaged cell temperature [K]", "Local potenital difference [V]", "Current collector current density [A.m-2]", "Terminal voltage [V]", diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index 04ec8ba70a..be33fb9688 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -122,6 +122,12 @@ def test_bad_options(self): pybamm.BaseBatteryModel({"surface form": "bad surface form"}) with self.assertRaisesRegex(pybamm.OptionError, "particle model"): pybamm.BaseBatteryModel({"particle": "bad particle"}) + with self.assertRaisesRegex(pybamm.OptionError, "option single"): + pybamm.BaseBatteryModel( + {"current collector": "single particle potential pair"} + ) + with self.assertRaisesRegex(pybamm.OptionError, "option set external"): + pybamm.BaseBatteryModel({"current collector": "set external potential"}) def test_build_twice(self): model = pybamm.lithium_ion.SPM() # need to pick a model to set vars and build diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 2788e6ae4c..ff214fa225 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -57,6 +57,10 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() + options = {"current collector": "set external potential", "dimensionality": 2} + model = pybamm.lithium_ion.SPM(options) + model.check_well_posedness() + def test_x_full_thermal_model_no_current_collector(self): options = {"thermal": "x-full"} model = pybamm.lithium_ion.SPM(options) diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py index 4630fc8418..f58ed75f1b 100644 --- a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py @@ -27,6 +27,25 @@ def test_public_functions(self): std_tests.test_all() +class TestSetPotetetialSPM2plus1DModel(unittest.TestCase): + def test_public_functions(self): + param = pybamm.standard_parameters_lithium_ion + submodel = cc.SetPotentialSingleParticle2plus1D(param) + val = pybamm.PrimaryBroadcast(0.0, "current collector") + variables = { + "X-averaged positive electrode open circuit potential": val, + "X-averaged negative electrode open circuit potential": val, + "X-averaged positive electrode reaction overpotential": val, + "X-averaged negative electrode reaction overpotential": val, + "X-averaged electrolyte overpotential": val, + "X-averaged positive electrode ohmic losses": val, + "X-averaged negative electrode ohmic losses": val + } + std_tests = tests.StandardSubModelTests(submodel, variables) + + std_tests.test_all() + + if __name__ == "__main__": print("Add -v for more debug output") import sys From 65ad72e5e8ec220e0d67c5197d3ff215b9a869ab Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Wed, 30 Oct 2019 16:37:49 +0000 Subject: [PATCH 071/122] #688 added docs --- docs/source/simulation.rst | 5 + pybamm/simulation.py | 159 +++++++++++++++++++-------- pybamm/solvers/scipy_solver.py | 2 +- tests/integration/test_simulation.py | 24 ---- tests/unit/test_simulation.py | 132 +++++++++++++++++++++- 5 files changed, 248 insertions(+), 74 deletions(-) create mode 100644 docs/source/simulation.rst delete mode 100644 tests/integration/test_simulation.py diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst new file mode 100644 index 0000000000..a50a67eb21 --- /dev/null +++ b/docs/source/simulation.rst @@ -0,0 +1,5 @@ +Simulation +========== + +.. autoclass:: pybamm.Simulation + :members: \ No newline at end of file diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 3164adc62a..dd0757ca17 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -4,14 +4,25 @@ class Simulation: + """A Simulation class for easy building and running of PyBaMM simulations. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model to be simulated + """ + def __init__(self, model): self.model = model self.set_defaults() self.reset() def set_defaults(self): + """ + A method to set all the simulation specs to default values for the + supplied model. + """ self.geometry = self._model.default_geometry - self._parameter_values = self._model.default_parameter_values self._submesh_types = self._model.default_submesh_types self._var_pts = self._model.default_var_pts @@ -20,44 +31,85 @@ def set_defaults(self): self._quick_plot_vars = None def reset(self): + """ + A method to reset a simulation back to its unprocessed state. + """ self.model = self._model_class(self._model_options) self.geometry = copy.deepcopy(self._unprocessed_geometry) self._mesh = None - self._discretization = None + self._disc = None self._solution = None self._status = "Unprocessed" def parameterize(self): - if self._status == "Unprocessed": - self._parameter_values.process_model(self._model) - self._parameter_values.process_geometry(self._geometry) - self._status = "Parameterized" - elif self._status == "Built": - # There is a function to update parameters in the model - # but this misses some geometric parameters. This class - # is for convenience and not speed so just re-build. - self.reset() - self.build() + """ + A method to set the parameters in the model and the associated geometry. If + the model has already been built or solved then this will first reset to the + unprocessed state and then set the parameter values. + """ + + if self._status != "Unprocessed": + return None + + self._parameter_values.process_model(self._model) + self._parameter_values.process_geometry(self._geometry) + self._status = "Parameterized" def build(self): - if self._status != "Built": - self.parameterize() - self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) - self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) - self._disc.process_model(self._model) - self._model_status = "Built" - - def solve(self, t_eval=None): + """ + A method to build the model as a pure linear algebra expression. If the model + has already been built or solved then this function will have no effect. + If you want to rebuild, first use "reset()". This method will + automatically set the parameters if they have not already been set. + """ + + if self._status == "Built" or self._status == "Solved": + return None + + self.parameterize() + self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) + self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) + self._disc.process_model(self._model) + self._status = "Built" + + def solve(self, t_eval=None, solver=None): + """ + A method to solve the model. This method will automatically build + and set the model parameters if not already done so. + + Parameters + ---------- + t_eval : numeric type (optional) + The times at which to compute the solution + solver : :class:`pybamm.BaseSolver` + The solver to use to solve the model. + """ self.build() if t_eval is None: t_eval = np.linspace(0, 1, 100) - self._solution = self.solver.solve(self._model, t_eval) + if solver is None: + solver = self.solver + + self._solution = solver.solve(self._model, t_eval) + self._status = "Solved" + + def plot(self, quick_plot_vars=None): + """ + A method to quickly plot the outputs of the simulation. + + Parameters + ---------- + quick_plot_vars: list + A list of the variables to plot. + """ + + if quick_plot_vars is None: + quick_plot_vars = self.quick_plot_vars - def plot(self): plot = pybamm.QuickPlot( - self._model, self._mesh, self._solution, self._quick_plot_vars + self._model, self._mesh, self._solution, quick_plot_vars ) plot.dynamic_plot() @@ -85,19 +137,12 @@ def geometry(self, geometry): self._unprocessed_geometry = copy.deepcopy(geometry) @property - def parameter_values(self): - return self.parameter_values - - @parameter_values.setter - def parameter_values(self, parameter_values): - self._parameter_values = parameter_values - - if self._status == "Parameterized": - self.reset() - self.parameterize() + def unprocessed_geometry(self): + return self._unprocessed_geometry - elif self._status == "Built": - self._parameter_values.update_model(self._model, self._disc) + @property + def parameter_values(self): + return self._parameter_values @property def submesh_types(self): @@ -115,15 +160,23 @@ def spatial_methods(self): def solver(self): return self._solver + @solver.setter + def solver(self, solver): + self._solver = solver + @property def quick_plot_vars(self): return self._quick_plot_vars + @quick_plot_vars.setter + def quick_plot_vars(self, quick_plot_vars): + self._quick_plot_vars = quick_plot_vars + @property def solution(self): return self._solution - def set_specs( + def specs( self, model_options=None, geometry=None, @@ -134,6 +187,32 @@ def set_specs( solver=None, quick_plot_vars=None, ): + """ + A method to set the various specs of the simulation. This method + automatically resets the model after the new specs have been set. + + Parameters + ---------- + model_options: dict (optional) + A dictionary of options to tweak the model you are using + geometry: :class:`pybamm.Geometry` (optional) + The geometry upon which to solve the model + parameter_values: dict (optional) + A dictionary of parameters and their corresponding numerical + values + submesh_types: dict (optional) + A dictionary of the types of submesh to use on each subdomain + var_pts: dict (optional) + A dictionary of the number of points used by each spatial + variable + spatial_methods: dict (optional) + A dictionary of the types of spatial method to use on each + domain (e.g. pybamm.FiniteVolume) + solver: :class:`pybamm.BaseSolver` + The solver to use to solve the model. + quick_plot_vars: list + A list of variables to plot automatically + """ if model_options: self._model_options = model_options @@ -154,10 +233,4 @@ def set_specs( if quick_plot_vars: self._quick_plot_vars = quick_plot_vars - if self._status == "Parameterized": - self.reset() - self.parameterize - elif self._status == "Built": - self.reset() - self.build() - + self.reset() diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index fd7afbf498..23e11483c5 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -83,7 +83,7 @@ def integrate( termination = "event" t_event = [] for time in sol.t_events: - if time: + if time.size > 0: t_event = np.append(t_event, np.max(time)) t_event = np.array([np.max(t_event)]) y_event = sol.sol(t_event) diff --git a/tests/integration/test_simulation.py b/tests/integration/test_simulation.py deleted file mode 100644 index a7ce9f02a6..0000000000 --- a/tests/integration/test_simulation.py +++ /dev/null @@ -1,24 +0,0 @@ -import pybamm -import unittest - - -class TestSimulation(unittest.TestCase): - def test_run_with_spm(self): - model = pybamm.lithium_ion.SPM() - sim = pybamm.Simulation(model) - sim.solve() - - def test_update_parameters(self): - model = pybamm.lithium_ion.SPM() - sim = pybamm.Simulation(model) - - sim.parameterize_model() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 450c0e5bd5..f1fed05f87 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -3,19 +3,139 @@ class TestSimulation(unittest.TestCase): - def test_set_model(self): + def test_basic_ops(self): model = pybamm.lithium_ion.SPM() sim = pybamm.Simulation(model) - self.assertEqual(sim.model, model) + self.assertEqual(model.__class__, sim._model_class) + self.assertEqual(model.options, sim._model_options) - def test_reset_model(self): + # check that the model is unprocessed + self.assertEqual(sim._status, "Unprocessed") + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + for val in list(sim.model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) - sim = pybamm.Simulation(pybamm.SPM()) + sim.parameterize() + self.assertEqual(sim._status, "Parameterized") + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + for val in list(sim.model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) - sim.discretize_model() + sim.build() + self.assertEqual(sim._status, "Built") + self.assertFalse(sim._mesh is None) + self.assertFalse(sim._disc is None) + for val in list(sim.model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) - sim.reset_model() + sim.parameterize() + self.assertEqual(sim._status, "Parameterized") + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + for val in list(sim.model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + sim.build() + sim.reset() + self.assertEqual(sim._status, "Unprocessed") + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + for val in list(sim.model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + def test_solve(self): + + sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) + sim.solve() + self.assertFalse(sim._solution is None) + self.assertEqual(sim._status, "Solved") + for val in list(sim.model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) + + sim.reset() + self.assertEqual(sim._status, "Unprocessed") + for val in list(sim.model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + self.assertEqual(sim._solution, None) # check can now re-parameterize model + + def test_reuse_commands(self): + + sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) + + sim.parameterize() + sim.parameterize() + + sim.build() + sim.build() + + sim.solve() + sim.solve() + + sim.build() + sim.solve() + sim.parameterize() + + def test_specs(self): + # test can rebuild after setting specs + sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) + sim.build() + + model_options = {"thermal": "lumped"} + sim.specs(model_options=model_options) + sim.build() + self.assertEqual(sim.model.options["thermal"], "lumped") + + params = sim.parameter_values + # normally is 0.0001 + params.update({"Negative electrode thickness [m]": 0.0002}) + sim.specs(parameter_values=params) + + self.assertEqual( + sim.parameter_values["Negative electrode thickness [m]"], 0.0002 + ) + sim.build() + + geometry = sim.unprocessed_geometry + custom_geometry = {} + x_n = pybamm.standard_spatial_vars.x_n + custom_geometry["negative electrode"] = { + "primary": { + x_n: {"min": pybamm.Scalar(0), "max": pybamm.geometric_parameters.l_n} + } + } + geometry.update(custom_geometry) + sim.specs(geometry=geometry) + sim.build() + + var_pts = sim.var_pts + var_pts[pybamm.standard_spatial_vars.x_n] = 5 + sim.specs(var_pts=var_pts) + sim.build() + + spatial_methods = sim.spatial_methods + # nothing to change this to at the moment but just reload in + sim.specs(spatial_methods=spatial_methods) + sim.build() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + unittest.main() + From 77778a6ff8cc1824f7dfbed5c8d7f0a8e53afb4c Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 16:43:31 +0000 Subject: [PATCH 072/122] #678 fix add subtract simplify bug --- pybamm/expression_tree/binary_operators.py | 20 +++++++++++++++---- .../test_operations/test_simplify.py | 11 ++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/pybamm/expression_tree/binary_operators.py b/pybamm/expression_tree/binary_operators.py index 70e9b1ea6d..0645d5b97b 100644 --- a/pybamm/expression_tree/binary_operators.py +++ b/pybamm/expression_tree/binary_operators.py @@ -277,9 +277,15 @@ def _binary_simplify(self, left, right): return left # Check matrices after checking scalars if is_matrix_zero(left): - return right + if isinstance(right, pybamm.Scalar): + return pybamm.Array(right.value * np.ones(left.shape_for_testing)) + else: + return right if is_matrix_zero(right): - return left + if isinstance(left, pybamm.Scalar): + return pybamm.Array(left.value * np.ones(right.shape_for_testing)) + else: + return left return pybamm.simplify_addition_subtraction(self.__class__, left, right) @@ -325,9 +331,15 @@ def _binary_simplify(self, left, right): return left # Check matrices after checking scalars if is_matrix_zero(left): - return -right + if isinstance(right, pybamm.Scalar): + return pybamm.Array(-right.value * np.ones(left.shape_for_testing)) + else: + return -right if is_matrix_zero(right): - return left + if isinstance(left, pybamm.Scalar): + return pybamm.Array(left.value * np.ones(right.shape_for_testing)) + else: + return left return pybamm.simplify_addition_subtraction(self.__class__, left, right) diff --git a/tests/unit/test_expression_tree/test_operations/test_simplify.py b/tests/unit/test_expression_tree/test_operations/test_simplify.py index 1d1f61c805..f84909706a 100644 --- a/tests/unit/test_expression_tree/test_operations/test_simplify.py +++ b/tests/unit/test_expression_tree/test_operations/test_simplify.py @@ -102,6 +102,17 @@ def myfunction(x, y): self.assertIsInstance((b - a).simplify(), pybamm.Scalar) self.assertEqual((b - a).simplify().evaluate(), 1) + # addition and subtraction with matrix zero + v = pybamm.Vector(np.zeros((10, 1))) + self.assertIsInstance((b + v).simplify(), pybamm.Array) + np.testing.assert_array_equal((b + v).simplify().evaluate(), np.ones((10, 1))) + self.assertIsInstance((v + b).simplify(), pybamm.Array) + np.testing.assert_array_equal((v + b).simplify().evaluate(), np.ones((10, 1))) + self.assertIsInstance((b - v).simplify(), pybamm.Array) + np.testing.assert_array_equal((b - v).simplify().evaluate(), np.ones((10, 1))) + self.assertIsInstance((v - b).simplify(), pybamm.Array) + np.testing.assert_array_equal((v - b).simplify().evaluate(), -np.ones((10, 1))) + # multiplication self.assertIsInstance((a * b).simplify(), pybamm.Scalar) self.assertEqual((a * b).simplify().evaluate(), 0) From 98dd8b12023137d66e76321e0d9161a2ebcedf4a Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Wed, 30 Oct 2019 16:55:12 +0000 Subject: [PATCH 073/122] #688 passes tests --- tests/unit/test_simulation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index f1fed05f87..75bf849bc2 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -35,6 +35,7 @@ def test_basic_ops(self): self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) + sim.reset() sim.parameterize() self.assertEqual(sim._status, "Parameterized") self.assertEqual(sim._mesh, None) From 74d05a4a73fe5e0151a27a88e6ff4eaaaf7520a1 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Wed, 30 Oct 2019 17:03:39 +0000 Subject: [PATCH 074/122] #688 added simulation to toctree --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 49b6913f16..d991331d6c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ Contents source/solvers/index source/processed_variable source/util + source/simulation Examples ======== From 699c27470394834514c52d3df3b66cd78199d540 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Wed, 30 Oct 2019 17:05:41 +0000 Subject: [PATCH 075/122] #688 updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07421aef02..92814cf09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Added Simulation class ([#693](https://github.com/pybamm-team/PyBaMM/pull/693)) - Added interface (via pybind11) to sundials with the IDA KLU sparse linear solver ([#657](https://github.com/pybamm-team/PyBaMM/pull/657)) - Add method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) - Add `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) From 17b34e5b162b1c39a2b8efda0887b33c2c0f604b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 17:10:21 +0000 Subject: [PATCH 076/122] #687 resolving negative electrode heat discrepency --- .../compare_comsol_thermal.py | 158 ++++++++++++------ 1 file changed, 108 insertions(+), 50 deletions(-) diff --git a/results/comsol_comparison/compare_comsol_thermal.py b/results/comsol_comparison/compare_comsol_thermal.py index 9c47f8053b..66ba94f5dd 100644 --- a/results/comsol_comparison/compare_comsol_thermal.py +++ b/results/comsol_comparison/compare_comsol_thermal.py @@ -150,16 +150,10 @@ def myinterp(t): plt.legend() pybamm_voltage = pybamm.ProcessedVariable( - pybamm_model.variables["Terminal voltage [V]"], - solution.t, - solution.y, - mesh=mesh, + pybamm_model.variables["Terminal voltage [V]"], solution.t, solution.y, mesh=mesh )(plot_times / tau) comsol_voltage = pybamm.ProcessedVariable( - comsol_model.variables["Terminal voltage [V]"], - solution.t, - solution.y, - mesh=mesh, + comsol_model.variables["Terminal voltage [V]"], solution.t, solution.y, mesh=mesh )(plot_times / tau) plt.figure() plt.plot(plot_times, pybamm_voltage, "-", label="PyBaMM") @@ -175,39 +169,39 @@ def myinterp(t): x = mesh.combine_submeshes(*whole_cell)[0].nodes -def comparison_plot(var, plot_times=None): +def whole_cell_by_domain_comparison_plot(var, plot_times=None): """ - Plot pybamm heat source (defined over whole cell) against comsol heat source + Plot pybamm variable (defined over whole cell) against comsol variable (defined by component) """ if plot_times is None: plot_times = comsol_variables["time"] - # Process pybamm heat source - pybamm_q = pybamm.ProcessedVariable( + # Process pybamm variable + pybamm_var = pybamm.ProcessedVariable( pybamm_model.variables[var], solution.t, solution.y, mesh=mesh ) - # Process comsol heat source in negative electrode - comsol_q_n = pybamm.ProcessedVariable( + # Process comsol variable in negative electrode + comsol_var_n = pybamm.ProcessedVariable( comsol_model.variables["Negative electrode " + var[0].lower() + var[1:]], solution.t, solution.y, mesh=mesh, ) - # Process comsol heat source in separator (if defined here) + # Process comsol variable in separator (if defined here) try: - comsol_q_s = pybamm.ProcessedVariable( + comsol_var_s = pybamm.ProcessedVariable( comsol_model.variables["Separator " + var[0].lower() + var[1:]], solution.t, solution.y, mesh=mesh, ) except KeyError: - comsol_q_s = None + comsol_var_s = None print("Variable " + var + " not defined in separator") - # Process comsol heat source in positive electrode - comsol_q_p = pybamm.ProcessedVariable( + # Process comsol variable in positive electrode + comsol_var_p = pybamm.ProcessedVariable( comsol_model.variables["Positive electrode " + var[0].lower() + var[1:]], solution.t, solution.y, @@ -215,7 +209,7 @@ def comparison_plot(var, plot_times=None): ) # Make plot - if comsol_q_s: + if comsol_var_s: n_cols = 3 else: n_cols = 2 @@ -224,21 +218,21 @@ def comparison_plot(var, plot_times=None): for ind, t in enumerate(plot_times): color = cmap(float(ind) / len(plot_times)) - ax[0].plot(x_n * L_x, pybamm_q(x=x_n, t=t / tau), "-", color=color) - ax[0].plot(x_n * L_x, comsol_q_n(x=x_n, t=t / tau), "o", color=color) - if comsol_q_s: - ax[1].plot(x_s * L_x, pybamm_q(x=x_s, t=t / tau), "-", color=color) - ax[1].plot(x_s * L_x, comsol_q_s(x=x_s, t=t / tau), "o", color=color) + ax[0].plot(x_n * L_x, pybamm_var(x=x_n, t=t / tau), "-", color=color) + ax[0].plot(x_n * L_x, comsol_var_n(x=x_n, t=t / tau), "o", color=color) + if comsol_var_s: + ax[1].plot(x_s * L_x, pybamm_var(x=x_s, t=t / tau), "-", color=color) + ax[1].plot(x_s * L_x, comsol_var_s(x=x_s, t=t / tau), "o", color=color) ax[n_cols - 1].plot( x_p * L_x, - pybamm_q(x=x_p, t=t / tau), + pybamm_var(x=x_p, t=t / tau), "-", color=color, label="PyBaMM" if ind == 0 else "", ) ax[n_cols - 1].plot( x_p * L_x, - comsol_q_p(x=x_p, t=t / tau), + comsol_var_p(x=x_p, t=t / tau), "o", color=color, label="COMSOL" if ind == 0 else "", @@ -246,7 +240,7 @@ def comparison_plot(var, plot_times=None): ax[0].set_xlabel("x_n") ax[0].set_ylabel(var) - if comsol_q_s: + if comsol_var_s: ax[1].set_xlabel("x_s") ax[1].set_ylabel(var) ax[n_cols - 1].set_xlabel("x_p") @@ -255,28 +249,80 @@ def comparison_plot(var, plot_times=None): plt.tight_layout() -def temperature_plot(plot_times=None): +def electrode_comparison_plot(var, plot_times=None): """ - Plot pybamm heat source (defined over whole cell) against comsol heat source - (defined by component) + Plot pybamm variable against comsol variable (both defined separately in the + negative and positive electrode) """ if plot_times is None: plot_times = comsol_variables["time"] - # Process pybamm heat source - pybamm_T = pybamm.ProcessedVariable( - pybamm_model.variables["Cell temperature [K]"], - solution.t, - solution.y, - mesh=mesh, + # Process pybamm variable in negative electrode + pybamm_var_n = pybamm.ProcessedVariable( + pybamm_model.variables["Negative " + var], solution.t, solution.y, mesh=mesh ) - # Process comsol heat source in negative electrode - comsol_T = pybamm.ProcessedVariable( - comsol_model.variables["Cell temperature [K]"], - solution.t, - solution.y, - mesh=mesh, + # Process pybamm variable in positive electrode + pybamm_var_p = pybamm.ProcessedVariable( + pybamm_model.variables["Positive " + var], solution.t, solution.y, mesh=mesh + ) + + # Process comsol variable in negative electrode + comsol_var_n = pybamm.ProcessedVariable( + comsol_model.variables["Negative " + var], solution.t, solution.y, mesh=mesh + ) + + # Process comsol variable in positive electrode + comsol_var_p = pybamm.ProcessedVariable( + comsol_model.variables["Positive " + var], solution.t, solution.y, mesh=mesh + ) + + # Make plot + fig, ax = plt.subplots(1, 2, figsize=(15, 8)) + cmap = plt.get_cmap("inferno") + + for ind, t in enumerate(plot_times): + color = cmap(float(ind) / len(plot_times)) + ax[0].plot(x_n * L_x, pybamm_var_n(x=x_n, t=t / tau), "-", color=color) + ax[0].plot(x_n * L_x, comsol_var_n(x=x_n, t=t / tau), "o", color=color) + ax[1].plot( + x_p * L_x, + pybamm_var_p(x=x_p, t=t / tau), + "-", + color=color, + label="PyBaMM" if ind == 0 else "", + ) + ax[1].plot( + x_p * L_x, + comsol_var_p(x=x_p, t=t / tau), + "o", + color=color, + label="COMSOL" if ind == 0 else "", + ) + + ax[0].set_xlabel("x_n") + ax[0].set_ylabel(var) + ax[1].set_xlabel("x_p") + ax[1].set_ylabel(var) + plt.legend() + plt.tight_layout() + + +def whole_cell_comparison_plot(var, plot_times=None): + """ + Plot pybamm variable against comsol variable (both defined over whole cell) + """ + if plot_times is None: + plot_times = comsol_variables["time"] + + # Process pybamm variable + pybamm_var = pybamm.ProcessedVariable( + pybamm_model.variables[var], solution.t, solution.y, mesh=mesh + ) + + # Process comsol variable + comsol_var = pybamm.ProcessedVariable( + comsol_model.variables[var], solution.t, solution.y, mesh=mesh ) # Make plot @@ -287,29 +333,41 @@ def temperature_plot(plot_times=None): color = cmap(float(ind) / len(plot_times)) plt.plot( x * L_x, - pybamm_T(x=x, t=t / tau), + pybamm_var(x=x, t=t / tau), "-", color=color, label="PyBaMM" if ind == 0 else "", ) plt.plot( x * L_x, - comsol_T(x=x, t=t / tau), + comsol_var(x=x, t=t / tau), "o", color=color, label="COMSOL" if ind == 0 else "", ) plt.xlabel("x") - plt.ylabel("Temperature [K]") + plt.ylabel(var) plt.legend() plt.tight_layout() # Make plots plot_times = comsol_variables["time"][0::10] -comparison_plot("Irreversible electrochemical heating [W.m-3]", plot_times=plot_times) -comparison_plot("Reversible heating [W.m-3]", plot_times=plot_times) -comparison_plot("Total heating [W.m-3]", plot_times=plot_times) -# temperature_plot(plot_times) +# heat sources +whole_cell_by_domain_comparison_plot( + "Irreversible electrochemical heating [W.m-3]", plot_times=plot_times +) +whole_cell_by_domain_comparison_plot( + "Reversible heating [W.m-3]", plot_times=plot_times +) +whole_cell_by_domain_comparison_plot("Total heating [W.m-3]", plot_times=plot_times) +# potentials +electrode_comparison_plot("electrode potential [V]", plot_times=plot_times) +whole_cell_comparison_plot("Electrolyte potential [V]", plot_times=plot_times) +# concentrations +electrode_comparison_plot( + "particle surface concentration [mol.m-3]", plot_times=plot_times +) +whole_cell_comparison_plot("Electrolyte concentration [mol.m-3]", plot_times=plot_times) plt.show() From bb9987478e209b22b5e67aced9a89b2f825b99c3 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 30 Oct 2019 17:19:39 +0000 Subject: [PATCH 077/122] #548 extra options tests --- .../test_full_battery_models/test_lithium_ion/test_spm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index ff214fa225..6600c68c9d 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -53,6 +53,10 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() + options = {"current collector": "set external potential", "dimensionality": 0} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.SPM(options) + options = {"current collector": "set external potential", "dimensionality": 1} model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() From 2515a11668a11476e4f8fe188478129e9b15287c Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 30 Oct 2019 16:17:17 -0400 Subject: [PATCH 078/122] #684 debugging example --- examples/scripts/compare-dae-solver.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/scripts/compare-dae-solver.py b/examples/scripts/compare-dae-solver.py index e4dff68a3d..d5256bbeea 100644 --- a/examples/scripts/compare-dae-solver.py +++ b/examples/scripts/compare-dae-solver.py @@ -16,7 +16,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 50, var.x_s: 50, var.x_p: 50, var.r_n: 20, var.r_p: 20} +var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model @@ -31,7 +31,10 @@ scikits_sol = pybamm.ScikitsDaeSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) # plot +import warnings + +warnings.simplefilter("error") models = [model, model, model] -solutions = [scikits_sol, klu_sol, casadi_sol] -plot = pybamm.QuickPlot(models, mesh, solutions) -plot.dynamic_plot() +solutions = [casadi_sol, klu_sol, casadi_sol] +# plot = pybamm.QuickPlot(models, mesh, solutions) +# plot.dynamic_plot() From 41d1a7353825aa21cbc420a2006db3fc6343a99a Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 30 Oct 2019 23:22:31 -0400 Subject: [PATCH 079/122] #684 fix weird nans bug --- .gitignore | 1 + examples/scripts/compare-dae-solver.py | 9 +++------ pybamm/processed_variable.py | 1 + pybamm/quick_plot.py | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 037defc395..d59641db3c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.tmp *.png /local/ +*.DS_Store # don't ignore important .txt files !requirements* diff --git a/examples/scripts/compare-dae-solver.py b/examples/scripts/compare-dae-solver.py index d5256bbeea..a092a78652 100644 --- a/examples/scripts/compare-dae-solver.py +++ b/examples/scripts/compare-dae-solver.py @@ -16,7 +16,7 @@ # set mesh var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} +var_pts = {var.x_n: 50, var.x_s: 50, var.x_p: 50, var.r_n: 20, var.r_p: 20} mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model @@ -31,10 +31,7 @@ scikits_sol = pybamm.ScikitsDaeSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) # plot -import warnings - -warnings.simplefilter("error") models = [model, model, model] solutions = [casadi_sol, klu_sol, casadi_sol] -# plot = pybamm.QuickPlot(models, mesh, solutions) -# plot.dynamic_plot() +plot = pybamm.QuickPlot(models, mesh, solutions) +plot.dynamic_plot() diff --git a/pybamm/processed_variable.py b/pybamm/processed_variable.py index a4db4a6462..d78730f85f 100644 --- a/pybamm/processed_variable.py +++ b/pybamm/processed_variable.py @@ -405,6 +405,7 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, warn=True): elif self.dimensions == 3: out = self.call_3D(t, x, r, y, z) if warn is True and np.isnan(out).any(): + raise ValueError pybamm.logger.warning( "Calling variable outside interpolation range (returns 'nan')" ) diff --git a/pybamm/quick_plot.py b/pybamm/quick_plot.py index b2470f0ac3..53281a900d 100644 --- a/pybamm/quick_plot.py +++ b/pybamm/quick_plot.py @@ -8,7 +8,7 @@ def ax_min(data): "Calculate appropriate minimum axis value for plotting" - data_min = np.min(data) + data_min = np.nanmin(data) if data_min <= 0: return 1.04 * data_min else: @@ -17,7 +17,7 @@ def ax_min(data): def ax_max(data): "Calculate appropriate maximum axis value for plotting" - data_max = np.max(data) + data_max = np.nanmax(data) if data_max <= 0: return 0.96 * data_max else: @@ -256,14 +256,14 @@ def reset_axis(self): # Get min and max y values y_min = np.min( [ - ax_min(var(self.ts[i], **{spatial_var_name: spatial_var_value})) + ax_min(var(self.ts[i], **{spatial_var_name: spatial_var_value}, warn=False)) for i, variable_list in enumerate(variable_lists) for var in variable_list ] ) y_max = np.max( [ - ax_max(var(self.ts[i], **{spatial_var_name: spatial_var_value})) + ax_max(var(self.ts[i], **{spatial_var_name: spatial_var_value}, warn=False)) for i, variable_list in enumerate(variable_lists) for var in variable_list ] From ca8648c9e2a1d9ac628fdc20268d6652c070ed31 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 30 Oct 2019 23:29:35 -0400 Subject: [PATCH 080/122] #684 move mode --- pybamm/solvers/casadi_solver.py | 45 ++++++++++--------- tests/unit/test_solvers/test_casadi_solver.py | 12 +++-- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 9e2f094d4b..a0b7a1d46d 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -16,6 +16,14 @@ class CasadiSolver(pybamm.DaeSolver): method : str, optional The method to use for solving the system ('cvodes', for ODEs, or 'idas', for DAEs). Default is 'idas'. + mode : str + How to solve the model (default is "safe"): + + - "fast": perform direct integration, without accounting for events. \ + Recommended when simulating a drive cycle or other simulation where \ + no events should be triggered. + - "safe": perform step-and-check integration, checking whether events have \ + been triggered. Recommended for simulations of a full charge or discharge. rtol : float, optional The relative tolerance for the solver (default is 1e-6). atol : float, optional @@ -37,6 +45,7 @@ class CasadiSolver(pybamm.DaeSolver): def __init__( self, method="idas", + mode="safe", rtol=1e-6, atol=1e-6, root_method="lm", @@ -45,11 +54,21 @@ def __init__( **extra_options, ): super().__init__(method, rtol, atol, root_method, root_tol) + if mode in ["safe", "fast"]: + self.mode = mode + else: + raise ValueError( + """ + invalid mode '{}'. Must be either 'safe', for solving with events, + or 'fast', for solving quickly without events""".format( + mode + ) + ) self.max_step_decrease_count = max_step_decrease_count self.extra_options = extra_options - self.name = "CasADi solver ({})".format(method) + self.name = "CasADi solver ({}) with '{}' mode".format(method, mode) - def solve(self, model, t_eval, mode="safe"): + def solve(self, model, t_eval): """ Execute the solver setup and calculate the solution of the model at specified times. @@ -61,15 +80,7 @@ def solve(self, model, t_eval, mode="safe"): initial_conditions t_eval : numeric type The times at which to compute the solution - mode : str - How to solve the model (default is "safe"): - - - "fast": perform direct integration, without accounting for events. \ - Recommended when simulating a drive cycle or other simulation where \ - no events should be triggered. - - "safe": perform step-and-check integration, checking whether events have \ - been triggered. Recommended for simulations of a full charge or discharge. - + Raises ------ :class:`pybamm.ValueError` @@ -78,14 +89,14 @@ def solve(self, model, t_eval, mode="safe"): If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - if mode == "fast": + if self.mode == "fast": # Solve model normally by calling the solve method from parent class return super().solve(model, t_eval) elif model.events == {}: pybamm.logger.info("No events found, running fast mode") # Solve model normally by calling the solve method from parent class return super().solve(model, t_eval) - elif mode == "safe": + elif self.mode == "safe": # Step-and-check timer = pybamm.Timer() self.set_up_casadi(model) @@ -165,14 +176,6 @@ def solve(self, model, t_eval, mode="safe"): ) ) return solution - else: - raise ValueError( - """ - invalid mode '{}'. Must be either 'safe', for solving with events, - or 'fast', for solving quickly without events""".format( - mode - ) - ) def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. In this class, we diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 6fc6f0d9bc..3355e684bb 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -61,11 +61,8 @@ def test_integrate_failure(self): warnings.simplefilter("default") def test_bad_mode(self): - solver = pybamm.CasadiSolver() - model = pybamm.BaseModel() - model.events = {1: 2} with self.assertRaisesRegex(ValueError, "invalid mode"): - solver.solve(model, None, "bad mode") + pybamm.CasadiSolver(mode="bad mode") def test_model_solver(self): # Create model @@ -88,6 +85,13 @@ def test_model_solver(self): np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + # Fast mode + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas", mode="fast") + t_eval = np.linspace(0, 1, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + def test_model_solver_events(self): # Create model model = pybamm.BaseModel() From 7daddaf33112fc5fe86c194c73db844f4926cb1b Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 09:58:35 +0000 Subject: [PATCH 081/122] #688 allowed init with specs --- pybamm/simulation.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pybamm/simulation.py b/pybamm/simulation.py index dd0757ca17..3572dc2a44 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -12,9 +12,27 @@ class Simulation: The model to be simulated """ - def __init__(self, model): + def __init__( + self, + model, + geometry=None, + parameter_values=None, + submesh_types=None, + var_pts=None, + spatial_methods=None, + solver=None, + quick_plot_vars=None, + ): self.model = model - self.set_defaults() + + self.geometry = geometry or model.default_geometry + self._parameter_values = parameter_values or model.default_parameter_values + self._submesh_types = submesh_types or model.default_submesh_types + self._var_pts = var_pts or model.default_var_pts + self._spatial_methods = spatial_methods or model.default_spatial_methods + self._solver = solver or self._model.default_solver + self._quick_plot_vars = quick_plot_vars + self.reset() def set_defaults(self): From 6b2f94eddeac327e24d0e56c447ec4c0fee43752 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 10:04:00 +0000 Subject: [PATCH 082/122] #688 changed paraterize to set_parameters --- pybamm/simulation.py | 16 ++++++++++++---- tests/unit/test_simulation.py | 14 +++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 3572dc2a44..be562777a2 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -59,7 +59,7 @@ def reset(self): self._solution = None self._status = "Unprocessed" - def parameterize(self): + def set_parameters(self): """ A method to set the parameters in the model and the associated geometry. If the model has already been built or solved then this will first reset to the @@ -71,7 +71,7 @@ def parameterize(self): self._parameter_values.process_model(self._model) self._parameter_values.process_geometry(self._geometry) - self._status = "Parameterized" + self._status = "Parameters set" def build(self): """ @@ -84,7 +84,7 @@ def build(self): if self._status == "Built" or self._status == "Solved": return None - self.parameterize() + self.set_parameters() self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) self._disc.process_model(self._model) @@ -251,4 +251,12 @@ def specs( if quick_plot_vars: self._quick_plot_vars = quick_plot_vars - self.reset() + if ( + model_options + or geometry + or parameter_values + or submesh_types + or var_pts + or spatial_methods + ): + self.reset() diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 75bf849bc2..038e4b6265 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -19,8 +19,8 @@ def test_basic_ops(self): self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) - sim.parameterize() - self.assertEqual(sim._status, "Parameterized") + sim.set_parameters() + self.assertEqual(sim._status, "Parameters set") self.assertEqual(sim._mesh, None) self.assertEqual(sim._disc, None) for val in list(sim.model.rhs.values()): @@ -36,8 +36,8 @@ def test_basic_ops(self): self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) sim.reset() - sim.parameterize() - self.assertEqual(sim._status, "Parameterized") + sim.set_parameters() + self.assertEqual(sim._status, "Parameters set") self.assertEqual(sim._mesh, None) self.assertEqual(sim._disc, None) for val in list(sim.model.rhs.values()): @@ -76,8 +76,8 @@ def test_reuse_commands(self): sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) - sim.parameterize() - sim.parameterize() + sim.set_parameters() + sim.set_parameters() sim.build() sim.build() @@ -87,7 +87,7 @@ def test_reuse_commands(self): sim.build() sim.solve() - sim.parameterize() + sim.set_parameters() def test_specs(self): # test can rebuild after setting specs From 8f9d4f65eb26aaf5b35c8fa9ededc3021150d97d Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 10:05:53 +0000 Subject: [PATCH 083/122] #688 removed linear algebra phrase --- pybamm/simulation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pybamm/simulation.py b/pybamm/simulation.py index be562777a2..87dbf9046a 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -75,10 +75,11 @@ def set_parameters(self): def build(self): """ - A method to build the model as a pure linear algebra expression. If the model - has already been built or solved then this function will have no effect. - If you want to rebuild, first use "reset()". This method will - automatically set the parameters if they have not already been set. + A method to build the model into a system of matrices and vectors suitable for + performing numerical computations. If the model has already been built or + solved then this function will have no effect. If you want to rebuild, + first use "reset()". This method will automatically set the parameters + if they have not already been set. """ if self._status == "Built" or self._status == "Solved": From a15ae4f70f14a0bd8de99cc2cb3ecb5b9da99b77 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 10:09:40 +0000 Subject: [PATCH 084/122] #688 added readme --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2946fc01dc..8e4ca96df3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,17 @@ Python Battery Mathematical Modelling solves continuum models for batteries, usi ## How do I use PyBaMM? -PyBaMM comes with a number of [detailed examples](examples/notebooks/README.md), hosted here on +The easiest way to use PyBaMM is to run a 1C constant-current discharge with a model of your choice with all the default settings: +```python3 +import pybamm +model = pybamm.lithium_ion.DFN() # Doyle-Fuller-Newman model +sim = pybamm.Simulation(model) +sim.solve() +sim.plot() +``` +However, much greater customisation is available. It is possible to change the physics, parameter values, geometry, submesh type, number of submesh points, methods for spatial discretisation and solver for integration (see DFN [script](examples/scripts/DFN.py) or [notebook](examples/notebooks/models/dfn.ipynb)). + +Further details can be found in a number of [detailed examples](examples/notebooks/README.md), hosted here on github. In addition, there is a [full API documentation](http://pybamm.readthedocs.io/), hosted on [Read The Docs](readthedocs.io). A set of slides giving an overview of PyBaMM can be found From b550d7462ae169655b234ea49d0fa0db294f6184 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 10:29:06 +0000 Subject: [PATCH 085/122] #688 added inplace to parameter values --- pybamm/parameters/parameter_values.py | 23 ++++++++++++++++++++--- pybamm/simulation.py | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 5aff085d3e..71a12246d9 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -211,13 +211,13 @@ def check_and_update_parameter_values(self, values): values["C-rate"] = float(values["Typical current [A]"]) / capacity return values - def process_model(self, model, processing="process"): + def process_model(self, unprocessed_model, processing="process", inplace=True): """Assign parameter values to a model. Currently inplace, could be changed to return a new model. Parameters ---------- - model : :class:`pybamm.BaseModel` + unprocessed_model : :class:`pybamm.BaseModel` Model to assign parameter values for processing : str, optional Flag to indicate how to process model (default 'process') @@ -226,6 +226,9 @@ def process_model(self, model, processing="process"): and replace any Parameter with a Value) * 'update': Calls :meth:`update_scalars()` for use on already-processed \ model (update the value of any Scalars in the expression tree.) + inplace: bool, optional + If True, replace the parameters in the model in place. Otherwise, return a + new model with parameter values set. Default is True. Raises ------ @@ -233,7 +236,21 @@ def process_model(self, model, processing="process"): If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - pybamm.logger.info("Start setting parameters for {}".format(model.name)) + pybamm.logger.info("Start setting parameters for {}".format(unprocessed_model.name)) + + # set up inplace vs not inplace + if inplace: + # any changes to model_disc attributes will change model attributes + # since they point to the same object + model = unprocessed_model + else: + # create a blank model so that original model is unchanged + model = pybamm.BaseModel() + model.name = unprocessed_model.name + model.options = unprocessed_model.options + model.use_jacobian = unprocessed_model.use_jacobian + model.use_simplify = unprocessed_model.use_simplify + model.convert_to_format = unprocessed_model.convert_to_format if len(model.rhs) == 0 and len(model.algebraic) == 0: raise pybamm.ModelError("Cannot process parameters for empty model") diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 87dbf9046a..336b4fa421 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -88,7 +88,7 @@ def build(self): self.set_parameters() self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) - self._disc.process_model(self._model) + self._built_model = self._disc.process_model(self._model, inplace=False) self._status = "Built" def solve(self, t_eval=None, solver=None): From f14d7dcffde5c4ea283d7a1aaa141c90828a878a Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 10:41:56 +0000 Subject: [PATCH 086/122] #688 added inplace for parameters --- pybamm/parameters/parameter_values.py | 20 +++++++++++-------- .../test_parameters/test_update_parameters.py | 11 ++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 71a12246d9..20e8cdce20 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -236,7 +236,9 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - pybamm.logger.info("Start setting parameters for {}".format(unprocessed_model.name)) + pybamm.logger.info( + "Start setting parameters for {}".format(unprocessed_model.name) + ) # set up inplace vs not inplace if inplace: @@ -252,7 +254,7 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): model.use_simplify = unprocessed_model.use_simplify model.convert_to_format = unprocessed_model.convert_to_format - if len(model.rhs) == 0 and len(model.algebraic) == 0: + if len(unprocessed_model.rhs) == 0 and len(unprocessed_model.algebraic) == 0: raise pybamm.ModelError("Cannot process parameters for empty model") if processing == "process": @@ -260,13 +262,13 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): elif processing == "update": processing_function = self.update_scalars - for variable, equation in model.rhs.items(): + for variable, equation in unprocessed_model.rhs.items(): pybamm.logger.debug( "{} parameters for {!r} (rhs)".format(processing.capitalize(), variable) ) model.rhs[variable] = processing_function(equation) - for variable, equation in model.algebraic.items(): + for variable, equation in unprocessed_model.algebraic.items(): pybamm.logger.debug( "{} parameters for {!r} (algebraic)".format( processing.capitalize(), variable @@ -274,7 +276,7 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): ) model.algebraic[variable] = processing_function(equation) - for variable, equation in model.initial_conditions.items(): + for variable, equation in unprocessed_model.initial_conditions.items(): pybamm.logger.debug( "{} parameters for {!r} (initial conditions)".format( processing.capitalize(), variable @@ -287,7 +289,7 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): # small number of variables, e.g. {"negative tab": neg. tab bc, # "positive tab": pos. tab bc "no tab": no tab bc}. new_boundary_conditions = {} - for variable, bcs in model.boundary_conditions.items(): + for variable, bcs in unprocessed_model.boundary_conditions.items(): processed_variable = processing_function(variable) new_boundary_conditions[processed_variable] = {} for side in ["left", "right", "negative tab", "positive tab", "no tab"]: @@ -305,14 +307,14 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): model.boundary_conditions = new_boundary_conditions - for variable, equation in model.variables.items(): + for variable, equation in unprocessed_model.variables.items(): pybamm.logger.debug( "{} parameters for {!r} (variables)".format( processing.capitalize(), variable ) ) model.variables[variable] = processing_function(equation) - for event, equation in model.events.items(): + for event, equation in unprocessed_model.events.items(): pybamm.logger.debug( "{} parameters for event '{}''".format(processing.capitalize(), event) ) @@ -320,6 +322,8 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): pybamm.logger.info("Finish setting parameters for {}".format(model.name)) + return model + def update_model(self, model, disc): """Process a discretised model. Currently inplace, could be changed to return a new model. diff --git a/tests/unit/test_parameters/test_update_parameters.py b/tests/unit/test_parameters/test_update_parameters.py index ccc13a3c7e..bab24a8f42 100644 --- a/tests/unit/test_parameters/test_update_parameters.py +++ b/tests/unit/test_parameters/test_update_parameters.py @@ -94,6 +94,17 @@ def test_update_model(self): # results should be different self.assertNotEqual(np.linalg.norm(Y1 - Y3), 0) + def test_inplace(self): + model = pybamm.lithium_ion.SPM() + param = model.default_parameter_values + new_model = param.process_model(model, inplace=False) + + for val in list(model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + + for val in list(new_model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + def test_update_geometry(self): # test on simple lead-acid model model1 = pybamm.lead_acid.LOQS() From ffa11b09a00da6caf160b902725a4bb309c63bdc Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 10:56:12 +0000 Subject: [PATCH 087/122] #688 updated sim to use inplace instead of status --- pybamm/simulation.py | 24 ++++++++++++++++-------- tests/unit/test_simulation.py | 22 ++++++++++------------ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 336b4fa421..7cce647837 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -54,10 +54,11 @@ def reset(self): """ self.model = self._model_class(self._model_options) self.geometry = copy.deepcopy(self._unprocessed_geometry) + self._model_with_set_params = None + self._built_model = None self._mesh = None self._disc = None self._solution = None - self._status = "Unprocessed" def set_parameters(self): """ @@ -66,12 +67,13 @@ def set_parameters(self): unprocessed state and then set the parameter values. """ - if self._status != "Unprocessed": + if self.model_with_set_params: return None - self._parameter_values.process_model(self._model) + self._model_with_set_params = self._parameter_values.process_model( + self._model, inplace=True + ) self._parameter_values.process_geometry(self._geometry) - self._status = "Parameters set" def build(self): """ @@ -82,14 +84,13 @@ def build(self): if they have not already been set. """ - if self._status == "Built" or self._status == "Solved": + if self.built_model: return None self.set_parameters() self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) self._built_model = self._disc.process_model(self._model, inplace=False) - self._status = "Built" def solve(self, t_eval=None, solver=None): """ @@ -111,8 +112,7 @@ def solve(self, t_eval=None, solver=None): if solver is None: solver = self.solver - self._solution = solver.solve(self._model, t_eval) - self._status = "Solved" + self._solution = solver.solve(self.built_model, t_eval) def plot(self, quick_plot_vars=None): """ @@ -142,6 +142,14 @@ def model(self, model): self._model_class = model.__class__ self._model_options = model.options + @property + def model_with_set_params(self): + return self._model_with_set_params + + @property + def built_model(self): + return self._built_model + @property def model_options(self): return self._model_options diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 038e4b6265..8dd3aa1b36 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -12,7 +12,6 @@ def test_basic_ops(self): self.assertEqual(model.options, sim._model_options) # check that the model is unprocessed - self.assertEqual(sim._status, "Unprocessed") self.assertEqual(sim._mesh, None) self.assertEqual(sim._disc, None) for val in list(sim.model.rhs.values()): @@ -20,35 +19,35 @@ def test_basic_ops(self): self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) sim.set_parameters() - self.assertEqual(sim._status, "Parameters set") self.assertEqual(sim._mesh, None) self.assertEqual(sim._disc, None) - for val in list(sim.model.rhs.values()): + for val in list(sim.model_with_set_params.rhs.values()): self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) sim.build() - self.assertEqual(sim._status, "Built") self.assertFalse(sim._mesh is None) self.assertFalse(sim._disc is None) - for val in list(sim.model.rhs.values()): + for val in list(sim.built_model.rhs.values()): self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) sim.reset() sim.set_parameters() - self.assertEqual(sim._status, "Parameters set") self.assertEqual(sim._mesh, None) self.assertEqual(sim._disc, None) - for val in list(sim.model.rhs.values()): + self.assertEqual(sim.built_model, None) + + for val in list(sim.model_with_set_params.rhs.values()): self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) sim.build() sim.reset() - self.assertEqual(sim._status, "Unprocessed") self.assertEqual(sim._mesh, None) self.assertEqual(sim._disc, None) + self.assertEqual(sim.model_with_set_params, None) + self.assertEqual(sim.built_model, None) for val in list(sim.model.rhs.values()): self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) @@ -58,19 +57,18 @@ def test_solve(self): sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) sim.solve() self.assertFalse(sim._solution is None) - self.assertEqual(sim._status, "Solved") - for val in list(sim.model.rhs.values()): + for val in list(sim.built_model.rhs.values()): self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) sim.reset() - self.assertEqual(sim._status, "Unprocessed") + self.assertEqual(sim.model_with_set_params, None) + self.assertEqual(sim.built_model, None) for val in list(sim.model.rhs.values()): self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) self.assertEqual(sim._solution, None) - # check can now re-parameterize model def test_reuse_commands(self): From b377dadcdd7c74ba9f4cd04aa617077cc5c68c81 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Thu, 31 Oct 2019 11:01:26 +0000 Subject: [PATCH 088/122] #678 add temp depend to backward tafel --- .../submodels/interface/kinetics/tafel.py | 4 +- .../compare_comsol_thermal.py | 373 ------------------ results/comsol_comparison/load_comsol_data.py | 142 ------- 3 files changed, 2 insertions(+), 517 deletions(-) delete mode 100644 results/comsol_comparison/compare_comsol_thermal.py delete mode 100644 results/comsol_comparison/load_comsol_data.py diff --git a/pybamm/models/submodels/interface/kinetics/tafel.py b/pybamm/models/submodels/interface/kinetics/tafel.py index a263657353..26ab18e807 100644 --- a/pybamm/models/submodels/interface/kinetics/tafel.py +++ b/pybamm/models/submodels/interface/kinetics/tafel.py @@ -82,5 +82,5 @@ class BackwardTafel(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): - return -j0 * pybamm.exp(-(ne / 2) * eta_r) + def _get_kinetics(self, j0, ne, eta_r, T): + return -j0 * pybamm.exp(-(ne / (2 * (1 + self.param.Theta * T))) * eta_r) diff --git a/results/comsol_comparison/compare_comsol_thermal.py b/results/comsol_comparison/compare_comsol_thermal.py deleted file mode 100644 index 66ba94f5dd..0000000000 --- a/results/comsol_comparison/compare_comsol_thermal.py +++ /dev/null @@ -1,373 +0,0 @@ -import pybamm -import numpy as np -import os -import pickle -import scipy.interpolate as interp -import matplotlib.pyplot as plt - -# change working directory to the root of pybamm -os.chdir(pybamm.root_dir()) - -"-----------------------------------------------------------------------------" -"Load comsol data" - -comsol_variables = pickle.load( - open("input/comsol_results/comsol_thermal_1C.pickle", "rb") -) - -"-----------------------------------------------------------------------------" -"Create and solve pybamm model" - -# load model and geometry -pybamm.set_logging_level("INFO") -options = {"thermal": "x-full"} -pybamm_model = pybamm.lithium_ion.DFN(options) -geometry = pybamm_model.default_geometry - -# load parameters and process model and geometry -param = pybamm_model.default_parameter_values -param.process_model(pybamm_model) -param.process_geometry(geometry) - -# create mesh -var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 101, var.x_s: 51, var.x_p: 101, var.r_n: 31, var.r_p: 31} -mesh = pybamm.Mesh(geometry, pybamm_model.default_submesh_types, var_pts) - -# discretise model -disc = pybamm.Discretisation(mesh, pybamm_model.default_spatial_methods) -disc.process_model(pybamm_model) - -# discharge timescale -tau = param.process_symbol( - pybamm.standard_parameters_lithium_ion.tau_discharge -).evaluate(0) - -# solve model at comsol times -time = comsol_variables["time"] / tau -solver = pybamm.CasadiSolver() -solution = solver.solve(pybamm_model, time) - -"-----------------------------------------------------------------------------" -"Make Comsol 'model' for comparison" - -whole_cell = ["negative electrode", "separator", "positive electrode"] -comsol_t = comsol_variables["time"] -L_x = param.evaluate(pybamm.standard_parameters_lithium_ion.L_x) - - -def get_interp_fun(variable, domain): - """ - Create a :class:`pybamm.Function` object using the variable, to allow plotting with - :class:`'pybamm.QuickPlot'` (interpolate in space to match edges, and then create - function to interpolate in time) - """ - if domain == ["negative electrode"]: - comsol_x = comsol_variables["x_n"] - elif domain == ["separator"]: - comsol_x = comsol_variables["x_s"] - elif domain == ["positive electrode"]: - comsol_x = comsol_variables["x_p"] - elif domain == whole_cell: - comsol_x = comsol_variables["x"] - # Make sure to use dimensional space - pybamm_x = mesh.combine_submeshes(*domain)[0].nodes * L_x - variable = interp.interp1d(comsol_x, variable, axis=0)(pybamm_x) - - def myinterp(t): - return interp.interp1d(comsol_t, variable)(t)[:, np.newaxis] - - # Make sure to use dimensional time - fun = pybamm.Function(myinterp, pybamm.t * tau) - fun.domain = domain - return fun - - -comsol_c_n_surf = get_interp_fun(comsol_variables["c_n_surf"], ["negative electrode"]) -comsol_c_e = get_interp_fun(comsol_variables["c_e"], whole_cell) -comsol_c_p_surf = get_interp_fun(comsol_variables["c_p_surf"], ["positive electrode"]) -comsol_phi_n = get_interp_fun(comsol_variables["phi_n"], ["negative electrode"]) -comsol_phi_e = get_interp_fun(comsol_variables["phi_e"], whole_cell) -comsol_phi_p = get_interp_fun(comsol_variables["phi_p"], ["positive electrode"]) -comsol_voltage = interp.interp1d(comsol_t, comsol_variables["voltage"]) -comsol_temperature = get_interp_fun(comsol_variables["temperature"], whole_cell) -comsol_temperature_av = interp.interp1d( - comsol_t, comsol_variables["average temperature"] -) -comsol_q_irrev_n = get_interp_fun(comsol_variables["Q_irrev_n"], ["negative electrode"]) -comsol_q_irrev_p = get_interp_fun(comsol_variables["Q_irrev_p"], ["positive electrode"]) -comsol_q_rev_n = get_interp_fun(comsol_variables["Q_rev_n"], ["negative electrode"]) -comsol_q_rev_p = get_interp_fun(comsol_variables["Q_rev_p"], ["positive electrode"]) -comsol_q_total_n = get_interp_fun(comsol_variables["Q_total_n"], ["negative electrode"]) -comsol_q_total_s = get_interp_fun(comsol_variables["Q_total_s"], ["separator"]) -comsol_q_total_p = get_interp_fun(comsol_variables["Q_total_p"], ["positive electrode"]) - -# Create comsol model with dictionary of Matrix variables -comsol_model = pybamm.BaseModel() -comsol_model.variables = { - "Negative particle surface concentration [mol.m-3]": comsol_c_n_surf, - "Electrolyte concentration [mol.m-3]": comsol_c_e, - "Positive particle surface concentration [mol.m-3]": comsol_c_p_surf, - "Current [A]": pybamm_model.variables["Current [A]"], - "Negative electrode potential [V]": comsol_phi_n, - "Electrolyte potential [V]": comsol_phi_e, - "Positive electrode potential [V]": comsol_phi_p, - "Terminal voltage [V]": pybamm.Function(comsol_voltage, pybamm.t * tau), - "Cell temperature [K]": comsol_temperature, - "Volume-averaged cell temperature [K]": pybamm.Function( - comsol_temperature_av, pybamm.t * tau - ), - "Negative electrode irreversible electrochemical heating [W.m-3]": comsol_q_irrev_n, - "Positive electrode irreversible electrochemical heating [W.m-3]": comsol_q_irrev_p, - "Negative electrode reversible heating [W.m-3]": comsol_q_rev_n, - "Positive electrode reversible heating [W.m-3]": comsol_q_rev_p, - "Negative electrode total heating [W.m-3]": comsol_q_total_n, - "Separator total heating [W.m-3]": comsol_q_total_s, - "Positive electrode total heating [W.m-3]": comsol_q_total_p, -} - -"-----------------------------------------------------------------------------" -"Plot comparison" - -plot_times = comsol_variables["time"] -pybamm_T = pybamm.ProcessedVariable( - pybamm_model.variables["Volume-averaged cell temperature [K]"], - solution.t, - solution.y, - mesh=mesh, -)(plot_times / tau) -comsol_T = pybamm.ProcessedVariable( - comsol_model.variables["Volume-averaged cell temperature [K]"], - solution.t, - solution.y, - mesh=mesh, -)(plot_times / tau) -plt.figure() -plt.plot(plot_times, pybamm_T, "-", label="PyBaMM") -plt.plot(plot_times, comsol_T, "o", label="COMSOL") -plt.xlabel("t") -plt.ylabel("T") -plt.legend() - -pybamm_voltage = pybamm.ProcessedVariable( - pybamm_model.variables["Terminal voltage [V]"], solution.t, solution.y, mesh=mesh -)(plot_times / tau) -comsol_voltage = pybamm.ProcessedVariable( - comsol_model.variables["Terminal voltage [V]"], solution.t, solution.y, mesh=mesh -)(plot_times / tau) -plt.figure() -plt.plot(plot_times, pybamm_voltage, "-", label="PyBaMM") -plt.plot(plot_times, comsol_voltage, "o", label="COMSOL") -plt.xlabel("t") -plt.ylabel("Voltage [V]") -plt.legend() - -# Get mesh nodes -x_n = mesh.combine_submeshes(*["negative electrode"])[0].nodes -x_s = mesh.combine_submeshes(*["separator"])[0].nodes -x_p = mesh.combine_submeshes(*["positive electrode"])[0].nodes -x = mesh.combine_submeshes(*whole_cell)[0].nodes - - -def whole_cell_by_domain_comparison_plot(var, plot_times=None): - """ - Plot pybamm variable (defined over whole cell) against comsol variable - (defined by component) - """ - if plot_times is None: - plot_times = comsol_variables["time"] - - # Process pybamm variable - pybamm_var = pybamm.ProcessedVariable( - pybamm_model.variables[var], solution.t, solution.y, mesh=mesh - ) - - # Process comsol variable in negative electrode - comsol_var_n = pybamm.ProcessedVariable( - comsol_model.variables["Negative electrode " + var[0].lower() + var[1:]], - solution.t, - solution.y, - mesh=mesh, - ) - # Process comsol variable in separator (if defined here) - try: - comsol_var_s = pybamm.ProcessedVariable( - comsol_model.variables["Separator " + var[0].lower() + var[1:]], - solution.t, - solution.y, - mesh=mesh, - ) - except KeyError: - comsol_var_s = None - print("Variable " + var + " not defined in separator") - # Process comsol variable in positive electrode - comsol_var_p = pybamm.ProcessedVariable( - comsol_model.variables["Positive electrode " + var[0].lower() + var[1:]], - solution.t, - solution.y, - mesh=mesh, - ) - - # Make plot - if comsol_var_s: - n_cols = 3 - else: - n_cols = 2 - fig, ax = plt.subplots(1, n_cols, figsize=(15, 8)) - cmap = plt.get_cmap("inferno") - - for ind, t in enumerate(plot_times): - color = cmap(float(ind) / len(plot_times)) - ax[0].plot(x_n * L_x, pybamm_var(x=x_n, t=t / tau), "-", color=color) - ax[0].plot(x_n * L_x, comsol_var_n(x=x_n, t=t / tau), "o", color=color) - if comsol_var_s: - ax[1].plot(x_s * L_x, pybamm_var(x=x_s, t=t / tau), "-", color=color) - ax[1].plot(x_s * L_x, comsol_var_s(x=x_s, t=t / tau), "o", color=color) - ax[n_cols - 1].plot( - x_p * L_x, - pybamm_var(x=x_p, t=t / tau), - "-", - color=color, - label="PyBaMM" if ind == 0 else "", - ) - ax[n_cols - 1].plot( - x_p * L_x, - comsol_var_p(x=x_p, t=t / tau), - "o", - color=color, - label="COMSOL" if ind == 0 else "", - ) - - ax[0].set_xlabel("x_n") - ax[0].set_ylabel(var) - if comsol_var_s: - ax[1].set_xlabel("x_s") - ax[1].set_ylabel(var) - ax[n_cols - 1].set_xlabel("x_p") - ax[n_cols - 1].set_ylabel(var) - plt.legend() - plt.tight_layout() - - -def electrode_comparison_plot(var, plot_times=None): - """ - Plot pybamm variable against comsol variable (both defined separately in the - negative and positive electrode) - """ - if plot_times is None: - plot_times = comsol_variables["time"] - - # Process pybamm variable in negative electrode - pybamm_var_n = pybamm.ProcessedVariable( - pybamm_model.variables["Negative " + var], solution.t, solution.y, mesh=mesh - ) - - # Process pybamm variable in positive electrode - pybamm_var_p = pybamm.ProcessedVariable( - pybamm_model.variables["Positive " + var], solution.t, solution.y, mesh=mesh - ) - - # Process comsol variable in negative electrode - comsol_var_n = pybamm.ProcessedVariable( - comsol_model.variables["Negative " + var], solution.t, solution.y, mesh=mesh - ) - - # Process comsol variable in positive electrode - comsol_var_p = pybamm.ProcessedVariable( - comsol_model.variables["Positive " + var], solution.t, solution.y, mesh=mesh - ) - - # Make plot - fig, ax = plt.subplots(1, 2, figsize=(15, 8)) - cmap = plt.get_cmap("inferno") - - for ind, t in enumerate(plot_times): - color = cmap(float(ind) / len(plot_times)) - ax[0].plot(x_n * L_x, pybamm_var_n(x=x_n, t=t / tau), "-", color=color) - ax[0].plot(x_n * L_x, comsol_var_n(x=x_n, t=t / tau), "o", color=color) - ax[1].plot( - x_p * L_x, - pybamm_var_p(x=x_p, t=t / tau), - "-", - color=color, - label="PyBaMM" if ind == 0 else "", - ) - ax[1].plot( - x_p * L_x, - comsol_var_p(x=x_p, t=t / tau), - "o", - color=color, - label="COMSOL" if ind == 0 else "", - ) - - ax[0].set_xlabel("x_n") - ax[0].set_ylabel(var) - ax[1].set_xlabel("x_p") - ax[1].set_ylabel(var) - plt.legend() - plt.tight_layout() - - -def whole_cell_comparison_plot(var, plot_times=None): - """ - Plot pybamm variable against comsol variable (both defined over whole cell) - """ - if plot_times is None: - plot_times = comsol_variables["time"] - - # Process pybamm variable - pybamm_var = pybamm.ProcessedVariable( - pybamm_model.variables[var], solution.t, solution.y, mesh=mesh - ) - - # Process comsol variable - comsol_var = pybamm.ProcessedVariable( - comsol_model.variables[var], solution.t, solution.y, mesh=mesh - ) - - # Make plot - plt.figure(figsize=(15, 8)) - cmap = plt.get_cmap("inferno") - - for ind, t in enumerate(plot_times): - color = cmap(float(ind) / len(plot_times)) - plt.plot( - x * L_x, - pybamm_var(x=x, t=t / tau), - "-", - color=color, - label="PyBaMM" if ind == 0 else "", - ) - plt.plot( - x * L_x, - comsol_var(x=x, t=t / tau), - "o", - color=color, - label="COMSOL" if ind == 0 else "", - ) - - plt.xlabel("x") - plt.ylabel(var) - plt.legend() - plt.tight_layout() - - -# Make plots -plot_times = comsol_variables["time"][0::10] -# heat sources -whole_cell_by_domain_comparison_plot( - "Irreversible electrochemical heating [W.m-3]", plot_times=plot_times -) -whole_cell_by_domain_comparison_plot( - "Reversible heating [W.m-3]", plot_times=plot_times -) -whole_cell_by_domain_comparison_plot("Total heating [W.m-3]", plot_times=plot_times) -# potentials -electrode_comparison_plot("electrode potential [V]", plot_times=plot_times) -whole_cell_comparison_plot("Electrolyte potential [V]", plot_times=plot_times) -# concentrations -electrode_comparison_plot( - "particle surface concentration [mol.m-3]", plot_times=plot_times -) -whole_cell_comparison_plot("Electrolyte concentration [mol.m-3]", plot_times=plot_times) -plt.show() diff --git a/results/comsol_comparison/load_comsol_data.py b/results/comsol_comparison/load_comsol_data.py deleted file mode 100644 index a67ab9b8f3..0000000000 --- a/results/comsol_comparison/load_comsol_data.py +++ /dev/null @@ -1,142 +0,0 @@ -import pybamm -import os -import pandas as pd -import pickle -import numpy as np - -# change working directory the root of pybamm -os.chdir(pybamm.root_dir()) - -# set filepath for data and name of file to pickle to -path = "input/comsol_results_csv/thermal/1C/" -savefile = "input/comsol_results/comsol_thermal_1C.pickle" - -# time-voltage -comsol = pd.read_csv(path + "voltage.csv", sep=",", header=None) -comsol_time = comsol[0].values -comsol_time_npts = len(comsol_time) -comsol_voltage = comsol[1].values - -# negative electrode potential -comsol = pd.read_csv(path + "phi_s_n.csv", sep=",", header=None) -comsol_x_n_npts = int(len(comsol[0].values) / comsol_time_npts) -comsol_x_n = comsol[0].values[0:comsol_x_n_npts] -comsol_phi_n_vals = np.reshape( - comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" -) - -# negative particle surface concentration -comsol = pd.read_csv(path + "c_n_surf.csv", sep=",", header=None) -comsol_c_n_surf_vals = np.reshape( - comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" -) - -# positive electrode potential -comsol = pd.read_csv(path + "phi_s_p.csv", sep=",", header=None) -comsol_x_p_npts = int(len(comsol[0].values) / comsol_time_npts) -comsol_x_p = comsol[0].values[0:comsol_x_p_npts] -comsol_phi_p_vals = np.reshape( - comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" -) - -# positive particle surface concentration -comsol = pd.read_csv(path + "c_p_surf.csv", sep=",", header=None) -comsol_c_p_surf_vals = np.reshape( - comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" -) - -# electrolyte concentration -comsol = pd.read_csv(path + "c_e.csv", sep=",", header=None) -comsol_x_npts = int(len(comsol[0].values) / comsol_time_npts) -comsol_x = comsol[0].values[0:comsol_x_npts] -comsol_c_e_vals = np.reshape( - comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" -) - -# electrolyte potential -comsol = pd.read_csv(path + "phi_e.csv", sep=",", header=None) -comsol_phi_e_vals = np.reshape( - comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" -) - -# temperature -comsol = pd.read_csv(path + "temperature.csv", sep=",", header=None) -comsol_temp_vals = np.reshape( - comsol[1].values, (comsol_x_npts, comsol_time_npts), order="F" -) - -# average temperature -comsol_temp_av = np.mean(comsol_temp_vals, axis=0) - -# irreversible heat source in negative electrode -comsol = pd.read_csv(path + "q_irrev_n.csv", sep=",", header=None) -comsol_q_irrev_n_vals = np.reshape( - comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" -) - -# irreversible heat source in positive electrode -comsol = pd.read_csv(path + "q_irrev_p.csv", sep=",", header=None) -comsol_q_irrev_p_vals = np.reshape( - comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" -) - -# reversible heat source in negative electrode -comsol = pd.read_csv(path + "q_rev_n.csv", sep=",", header=None) -comsol_q_rev_n_vals = np.reshape( - comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" -) - -# reversible heat source in positive electrode -comsol = pd.read_csv(path + "q_rev_p.csv", sep=",", header=None) -comsol_q_rev_p_vals = np.reshape( - comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" -) - -# total heat source in negative electrode -comsol = pd.read_csv(path + "q_total_n.csv", sep=",", header=None) -comsol_q_total_n_vals = np.reshape( - comsol[1].values, (comsol_x_n_npts, comsol_time_npts), order="F" -) - -# total heat source in separator -comsol = pd.read_csv(path + "q_total_s.csv", sep=",", header=None) -comsol_x_s_npts = int(len(comsol[0].values) / comsol_time_npts) -comsol_x_s = comsol[0].values[0:comsol_x_s_npts] -comsol_q_total_s_vals = np.reshape( - comsol[1].values, (comsol_x_s_npts, comsol_time_npts), order="F" -) - -# total heat source in positive electrode -comsol = pd.read_csv(path + "q_total_p.csv", sep=",", header=None) -comsol_q_total_p_vals = np.reshape( - comsol[1].values, (comsol_x_p_npts, comsol_time_npts), order="F" -) - - -# add comsol variables to dict and pickle -comsol_variables = { - "time": comsol_time, - "x_n": comsol_x_n, - "x_s": comsol_x_s, - "x_p": comsol_x_p, - "x": comsol_x, - "voltage": comsol_voltage, - "phi_n": comsol_phi_n_vals, - "phi_p": comsol_phi_p_vals, - "phi_e": comsol_phi_e_vals, - "c_n_surf": comsol_c_n_surf_vals, - "c_p_surf": comsol_c_p_surf_vals, - "c_e": comsol_c_e_vals, - "temperature": comsol_temp_vals, - "average temperature": comsol_temp_av, - "Q_irrev_n": comsol_q_irrev_n_vals, - "Q_irrev_p": comsol_q_irrev_p_vals, - "Q_rev_n": comsol_q_rev_n_vals, - "Q_rev_p": comsol_q_rev_p_vals, - "Q_total_n": comsol_q_total_n_vals, - "Q_total_s": comsol_q_total_s_vals, - "Q_total_p": comsol_q_total_p_vals, -} - -with open(savefile, "wb") as f: - pickle.dump(comsol_variables, f, pickle.HIGHEST_PROTOCOL) From b89df67144192fc6f28e727d87a4cc1e4b4c9a5b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Thu, 31 Oct 2019 11:06:39 +0000 Subject: [PATCH 089/122] #678 update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b9c20626..52d195e7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ## Bug fixes +- Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add warning if `ProcessedVariable` is called outisde its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) - Improve the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) From 6f68df1d6f2c042fdb37a2923040b419c3e677eb Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 12:10:04 +0000 Subject: [PATCH 090/122] #688 fixed error for using inplace disctretization --- examples/scripts/SPMe.py | 7 ++++--- examples/scripts/run_simulation.py | 1 + pybamm/discretisations/discretisation.py | 4 ++-- pybamm/parameters/parameter_values.py | 4 ++-- pybamm/simulation.py | 10 +++++++++- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/examples/scripts/SPMe.py b/examples/scripts/SPMe.py index 2485dfd305..25659f7f9e 100644 --- a/examples/scripts/SPMe.py +++ b/examples/scripts/SPMe.py @@ -15,7 +15,7 @@ # load parameter values and process model and geometry param = model.default_parameter_values -param.process_model(model) +model2 = param.process_model(model) param.process_geometry(geometry) # set mesh @@ -23,12 +23,13 @@ # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) -disc.process_model(model) +model3 = disc.process_model(model2, inplace=False) +disc.process_model(model2, inplace=True) # solve model t_eval = np.linspace(0, 0.2, 100) solution = model.default_solver.solve(model, t_eval) # plot -plot = pybamm.QuickPlot(model, mesh, solution) +plot = pybamm.QuickPlot(model3, mesh, solution) plot.dynamic_plot() diff --git a/examples/scripts/run_simulation.py b/examples/scripts/run_simulation.py index ecb56d656d..09e2f3ed42 100644 --- a/examples/scripts/run_simulation.py +++ b/examples/scripts/run_simulation.py @@ -1,6 +1,7 @@ import pybamm model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model) sim.solve() sim.plot() diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index 284e375cb7..0c1ab0c6ab 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -122,8 +122,8 @@ def process_model(self, model, inplace=True): # since they point to the same object model_disc = model else: - # create a blank model so that original model is unchanged - model_disc = pybamm.BaseModel() + # create a model of the same class as the original model + model_disc = model.__class__(model.options) model_disc.name = model.name model_disc.options = model.options model_disc.use_jacobian = model.use_jacobian diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 20e8cdce20..69b1cc464f 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -246,8 +246,8 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): # since they point to the same object model = unprocessed_model else: - # create a blank model so that original model is unchanged - model = pybamm.BaseModel() + # create a blank model of the same class + model = model.__class__(model.options) model.name = unprocessed_model.name model.options = unprocessed_model.options model.use_jacobian = unprocessed_model.use_jacobian diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 7cce647837..774a5750b8 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -124,11 +124,19 @@ def plot(self, quick_plot_vars=None): A list of the variables to plot. """ + if self._solution is None: + raise ValueError( + "Model has not been solved, please solve the model before plotting." + ) + if quick_plot_vars is None: quick_plot_vars = self.quick_plot_vars plot = pybamm.QuickPlot( - self._model, self._mesh, self._solution, quick_plot_vars + self.built_model, + self._mesh, + self._solution, + output_variables=quick_plot_vars, ) plot.dynamic_plot() From 379c957fd2b6b59a5ee25a5e70697ea5e38b41e6 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Thu, 31 Oct 2019 12:29:01 +0000 Subject: [PATCH 091/122] #678 fix set temperature script --- results/2plus1D/set_temperature_spm_1plus1D.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/results/2plus1D/set_temperature_spm_1plus1D.py b/results/2plus1D/set_temperature_spm_1plus1D.py index fca61facbd..f6101a8824 100644 --- a/results/2plus1D/set_temperature_spm_1plus1D.py +++ b/results/2plus1D/set_temperature_spm_1plus1D.py @@ -66,7 +66,7 @@ def non_dim_temperature(temperature): "Negative current collector potential [V]", "Positive current collector potential [V]", "Current [A]", - "X-averaged total heating [A.V.m-3]", + "X-averaged total heating [W.m-3]", "X-averaged positive particle surface concentration [mol.m-3]", "X-averaged cell temperature [K]", ] @@ -145,16 +145,16 @@ def non_dim_temperature(temperature): for bat_id in range(nbat): plt.plot( solution1.t * t_hour, - processed_vars_step1["X-averaged total heating [A.V.m-3]"](solution1.t, z=z)[ + processed_vars_step1["X-averaged total heating [W.m-3]"](solution1.t, z=z)[ bat_id, : ], solution2.t * t_hour, - processed_vars_step2["X-averaged total heating [A.V.m-3]"](solution2.t, z=z)[ + processed_vars_step2["X-averaged total heating [W.m-3]"](solution2.t, z=z)[ bat_id, : ], ) plt.xlabel("t [hrs]") -plt.ylabel("X-averaged total heating [A.V.m-3]") +plt.ylabel("X-averaged total heating [W.m-3]") plt.yscale("log") # local concentration From 0b2f0fe35fbf8ea9686a0a43356ce627789930d6 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 12:30:01 +0000 Subject: [PATCH 092/122] #688 fixed issue in parameter values inplace --- examples/scripts/SPMe.py | 5 ++--- pybamm/parameters/parameter_values.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/scripts/SPMe.py b/examples/scripts/SPMe.py index 25659f7f9e..bec8e54099 100644 --- a/examples/scripts/SPMe.py +++ b/examples/scripts/SPMe.py @@ -15,7 +15,7 @@ # load parameter values and process model and geometry param = model.default_parameter_values -model2 = param.process_model(model) +param.process_model(model) param.process_geometry(geometry) # set mesh @@ -23,8 +23,7 @@ # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) -model3 = disc.process_model(model2, inplace=False) -disc.process_model(model2, inplace=True) +disc.process_model(model) # solve model t_eval = np.linspace(0, 0.2, 100) diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 69b1cc464f..9880746c00 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -247,7 +247,7 @@ def process_model(self, unprocessed_model, processing="process", inplace=True): model = unprocessed_model else: # create a blank model of the same class - model = model.__class__(model.options) + model = unprocessed_model.__class__(unprocessed_model.options) model.name = unprocessed_model.name model.options = unprocessed_model.options model.use_jacobian = unprocessed_model.use_jacobian From 5a35536f0f972b50d5ab00c6f02437289494df18 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Thu, 31 Oct 2019 08:43:57 -0400 Subject: [PATCH 093/122] #684 remove valueerror --- pybamm/processed_variable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pybamm/processed_variable.py b/pybamm/processed_variable.py index d78730f85f..a4db4a6462 100644 --- a/pybamm/processed_variable.py +++ b/pybamm/processed_variable.py @@ -405,7 +405,6 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, warn=True): elif self.dimensions == 3: out = self.call_3D(t, x, r, y, z) if warn is True and np.isnan(out).any(): - raise ValueError pybamm.logger.warning( "Calling variable outside interpolation range (returns 'nan')" ) From 0035628a33051f17584353114f76af5a0f7b51e1 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 14:09:36 +0000 Subject: [PATCH 094/122] #688 correct spme script --- examples/scripts/SPMe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/scripts/SPMe.py b/examples/scripts/SPMe.py index bec8e54099..2485dfd305 100644 --- a/examples/scripts/SPMe.py +++ b/examples/scripts/SPMe.py @@ -30,5 +30,5 @@ solution = model.default_solver.solve(model, t_eval) # plot -plot = pybamm.QuickPlot(model3, mesh, solution) +plot = pybamm.QuickPlot(model, mesh, solution) plot.dynamic_plot() From 19828a31fc5ccfe01f49307d716f5c3599629659 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 14:34:53 +0000 Subject: [PATCH 095/122] #697 Klu now accepts atol vector --- pybamm/solvers/c_solvers/idaklu.cpp | 12 ++++-------- pybamm/solvers/idaklu_solver.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 0e4735d8b6..776e3a1240 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -214,11 +214,12 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, residual_type res, jacobian_type jac, jac_get_type gjd, jac_get_type gjrv, jac_get_type gjcp, int nnz, event_type event, int number_of_events, int use_jacobian, np_array rhs_alg_id, - double abs_tol, double rel_tol) + np_array atol_np, double rel_tol) { auto t = t_np.unchecked<1>(); auto y0 = y0_np.unchecked<1>(); auto yp0 = yp0_np.unchecked<1>(); + auto atol = atol_np.unchecked<1>(); int number_of_states; number_of_states = y0_np.request().size; @@ -240,11 +241,13 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set initial value yval = N_VGetArrayPointer(yy); ypval = N_VGetArrayPointer(yp); + atval = N_VGetArrayPointer(avtol); int i; for (i = 0; i < number_of_states; i++) { yval[i] = y0[i]; ypval[i] = yp0[i]; + atval[i] = atol[i]; } // allocate memory for solver @@ -256,13 +259,6 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set tolerances rtol = RCONST(rel_tol); - atval = N_VGetArrayPointer(avtol); - - for (i = 0; i < number_of_states; i++) - { - atval[i] = - RCONST(abs_tol); // nb: this can be set differently for each state - } IDASVtolerances(ida_mem, rtol, avtol); diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 131f0fdd8b..eee08c7a87 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -77,6 +77,23 @@ def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian): rtol = self._rtol atol = self._atol + if isinstance(atol, float): + atol = atol * np.ones_like(y0) + elif isinstance(atol, list): + atol = np.array(atol) + elif isinstance(atol, np.ndarray): + pass + else: + raise pybamm.SolverError( + "Absolute tolerances must be a numpy array, float, or list" + ) + + if atol.shape != y0.shape: + raise pybamm.SolverError( + """Absolute tolerances must be either a scalar or a numpy arrray + of the same shape at y0""" + ) + if jacobian: jac_y0_t0 = jacobian(t_eval[0], y0) if sparse.issparse(jac_y0_t0): @@ -148,8 +165,8 @@ def rootfn(t, y): num_of_events, use_jac, ids, - rtol, atol, + rtol, ) t = sol.t From 34daf67c01b1ef4837c4733c92b4c5d28016e125 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Thu, 31 Oct 2019 15:32:32 +0000 Subject: [PATCH 096/122] #697 added ability to set tols by variable --- pybamm/solvers/idaklu_solver.py | 96 +++++++++++++++---- tests/integration/test_solvers/test_idaklu.py | 17 ++++ 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index eee08c7a87..df97be1830 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -44,6 +44,84 @@ def __init__( super().__init__("ida", rtol, atol, root_method, root_tol, max_steps) + def set_tolerances_by_variable(self, variables_with_tols, model): + """ + A method to set the absolute tolerances in the solver by state variable. + + Parameters + ---------- + variables_with_tols : dict + A dictionary with keys that are strings indicating the variable you + wish to set the tolerance of and values that are the tolerances. + + model : :class:`pybamm.BaseModel` + The model that is going to be solved. + """ + + size = model.concatenated_initial_conditions.size + self._check_atol_type(size) + for var, tol in variables_with_tols.items(): + variable = model.variables[var] + if isinstance(variable, pybamm.StateVector): + self.set_state_vec_tol(variable, tol) + elif isinstance(variable, pybamm.Concatenation): + for child in variable.children: + if isinstance(child, pybamm.StateVector): + self.set_state_vec_tol(child, tol) + else: + raise pybamm.SolverError( + """Can only set tolerances for state variables + or concatenations of state variables""" + ) + else: + raise pybamm.SolverError( + """Can only set tolerances for state variables or + concatenations of state variables""" + ) + + def set_state_vec_tol(self, state_vec, tol): + """ + A method to set the tolerances in the atol vector of a specific + state variable. + + Parameters + ---------- + state_vec : :class:`pybamm.StateVector` + The state vector to apply to the tolerance to + tol: float + The tolerance value + """ + slices = state_vec.y_slices[0] + self._atol[slices] = tol + + def _check_atol_type(self, size): + """ + This method checks that the atol vector is of the right shape and + type. + + Parameters + ---------- + size: int + The length of the atol vector + """ + + if isinstance(self._atol, float): + self._atol = self._atol * np.ones(size) + elif isinstance(self._atol, list): + self._atol = np.array(self._atol) + elif isinstance(self._atol, np.ndarray): + pass + else: + raise pybamm.SolverError( + "Absolute tolerances must be a numpy array, float, or list" + ) + + if self._atol.size != size: + raise pybamm.SolverError( + """Absolute tolerances must be either a scalar or a numpy arrray + of the same shape at y0""" + ) + def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian): """ Solve a DAE model defined by residuals with initial conditions y0. @@ -75,25 +153,9 @@ def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian): pybamm.SolverError("KLU requires events to be provided") rtol = self._rtol + self._check_atol_type(y0.size) atol = self._atol - if isinstance(atol, float): - atol = atol * np.ones_like(y0) - elif isinstance(atol, list): - atol = np.array(atol) - elif isinstance(atol, np.ndarray): - pass - else: - raise pybamm.SolverError( - "Absolute tolerances must be a numpy array, float, or list" - ) - - if atol.shape != y0.shape: - raise pybamm.SolverError( - """Absolute tolerances must be either a scalar or a numpy arrray - of the same shape at y0""" - ) - if jacobian: jac_y0_t0 = jacobian(t_eval[0], y0) if sparse.issparse(jac_y0_t0): diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index ea78201f74..ad83a7620e 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -19,6 +19,23 @@ def test_on_spme(self): solution = pybamm.IDAKLUSolver().solve(model, t_eval) np.testing.assert_array_less(1, solution.t.size) + def test_set_tol_by_variable(self): + model = pybamm.lithium_ion.SPMe() + geometry = model.default_geometry + param = model.default_parameter_values + param.process_model(model) + param.process_geometry(geometry) + mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) + disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + disc.process_model(model) + t_eval = np.linspace(0, 0.2, 100) + solver = pybamm.IDAKLUSolver() + + variable_tols = {"Electrolyte concentration": 1e-3} + solver.set_tolerances_by_variable(variable_tols, model) + + solver.solve(model, t_eval) + if __name__ == "__main__": print("Add -v for more debug output") From 472e77281d5e50741edbe486da55e7a30d93581d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 31 Oct 2019 11:52:52 -0400 Subject: [PATCH 097/122] [ci skip] update license year --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 5002a05e3c..e63d315d14 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, University of Oxford (University of Oxford means the Chancellor, Masters and Scholars of the University of Oxford, having an administrative office at Wellington Square, Oxford OX1 2JD, UK). +Copyright (c) 2017-2019, University of Oxford (University of Oxford means the Chancellor, Masters and Scholars of the University of Oxford, having an administrative office at Wellington Square, Oxford OX1 2JD, UK). All rights reserved. Redistribution and use in source and binary forms, with or without From 551f625b6990332524c487f2b87490dd41d231d2 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Fri, 1 Nov 2019 09:10:44 -0400 Subject: [PATCH 098/122] #684 flake8 --- pybamm/quick_plot.py | 16 ++++++- pybamm/solvers/casadi_solver.py | 10 ++--- tests/unit/test_solvers/test_casadi_solver.py | 43 +++++++++++++++---- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/pybamm/quick_plot.py b/pybamm/quick_plot.py index 53281a900d..472dc1237f 100644 --- a/pybamm/quick_plot.py +++ b/pybamm/quick_plot.py @@ -256,14 +256,26 @@ def reset_axis(self): # Get min and max y values y_min = np.min( [ - ax_min(var(self.ts[i], **{spatial_var_name: spatial_var_value}, warn=False)) + ax_min( + var( + self.ts[i], + **{spatial_var_name: spatial_var_value}, + warn=False + ) + ) for i, variable_list in enumerate(variable_lists) for var in variable_list ] ) y_max = np.max( [ - ax_max(var(self.ts[i], **{spatial_var_name: spatial_var_value}, warn=False)) + ax_max( + var( + self.ts[i], + **{spatial_var_name: spatial_var_value}, + warn=False + ) + ) for i, variable_list in enumerate(variable_lists) for var in variable_list ] diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index a0b7a1d46d..b31b5f32e8 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -34,7 +34,7 @@ class CasadiSolver(pybamm.DaeSolver): The tolerance for root-finding. Default is 1e-6. max_step_decrease_counts : float, optional The maximum number of times step size can be decreased before an error is - raised. Default is 10. + raised. Default is 5. extra_options : keyword arguments, optional Any extra keyword-arguments; these are passed directly to the CasADi integrator. Please consult `CasADi documentation `_ for @@ -50,7 +50,7 @@ def __init__( atol=1e-6, root_method="lm", root_tol=1e-6, - max_step_decrease_count=10, + max_step_decrease_count=5, **extra_options, ): super().__init__(method, rtol, atol, root_method, root_tol) @@ -80,7 +80,7 @@ def solve(self, model, t_eval): initial_conditions t_eval : numeric type The times at which to compute the solution - + Raises ------ :class:`pybamm.ValueError` @@ -128,7 +128,7 @@ def solve(self, model, t_eval): if solution is None: t = 0 else: - t = solution.t + t = solution.t[-1] raise pybamm.SolverError( """ Maximum number of decreased steps occurred at t={}. Try @@ -160,8 +160,6 @@ def solve(self, model, t_eval): else: # append solution from the current step to solution solution.append(current_step_sol) - if not hasattr(solution, "termination"): - solution.termination = "final time" # Calculate more exact termination reason solution.termination = self.get_termination_reason(solution, self.events) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 3355e684bb..89a3f3a175 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -45,18 +45,41 @@ def test_integrate_failure(self): y0 = np.array([1]) t_eval = np.linspace(0, 3, 100) - solver = pybamm.CasadiSolver( - rtol=1e-8, - atol=1e-8, - method="idas", - disable_internal_warnings=True, - regularity_check=False, - ) + solver = pybamm.CasadiSolver() problem = {"x": y, "ode": sqrt_decay} # Expect solver to fail when y goes negative with self.assertRaises(pybamm.SolverError): solver.integrate_casadi(problem, y0, t_eval) + # Set up as a model and solve + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -pybamm.Function(np.sqrt, var)} + model.initial_conditions = {var: 1} + # add events so that safe mode is used (won't be triggered) + model.events = {"10": var - 10} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve with failure at t=2 + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model, t_eval) + # Solve with failure at t=0 + model.initial_conditions = {var: 0} + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model, t_eval) + # Turn warnings back on warnings.simplefilter("default") @@ -85,8 +108,10 @@ def test_model_solver(self): np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - # Fast mode - solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas", mode="fast") + # Safe mode (enforce events that won't be triggered) + model.events = {"an event": var + 1} + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") t_eval = np.linspace(0, 1, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_equal(solution.t, t_eval) From 0ea9b6a884f1317ae6d86720ad33464bab0a30b0 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Fri, 1 Nov 2019 11:11:58 -0400 Subject: [PATCH 099/122] #679 make jupyter install by default and add version to package data --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8e9dac78cd..fbaebb09cb 100644 --- a/setup.py +++ b/setup.py @@ -27,10 +27,11 @@ def load_version(): description="Python Battery Mathematical Modelling.", long_description=readme, url="https://github.com/pybamm-team/PyBaMM", - # include_package_data=True, + include_package_data=True, packages=find_packages(include=("pybamm", "pybamm.*")), package_data={ "pybamm": [ + "./version", "../input/parameters/lithium-ion/*.csv", "../input/parameters/lithium-ion/*.py", "../input/parameters/lead-acid/*.csv", @@ -46,6 +47,7 @@ def load_version(): "autograd>=1.2", "scikit-fem>=0.2.0", "casadi>=3.5.0", + "jupyter", # For example notebooks # Note: Matplotlib is loaded for debug plots, but to ensure pybamm runs # on systems without an attached display, it should never be imported # outside of plot() methods. @@ -57,7 +59,6 @@ def load_version(): "dev": [ "flake8>=3", # For code style checking "black", # For code style auto-formatting - "jupyter", # For documentation and testing ], }, ) From dfb562f9b143717f5a071a86de440f993bad1e9d Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Mon, 4 Nov 2019 12:22:49 +0000 Subject: [PATCH 100/122] #697 changed to atol setter and added unit test --- pybamm/solvers/idaklu_solver.py | 10 ++++++++-- tests/unit/test_solvers/test_idaklu_solver.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index df97be1830..33d68c03cf 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -44,9 +44,15 @@ def __init__( super().__init__("ida", rtol, atol, root_method, root_tol, max_steps) - def set_tolerances_by_variable(self, variables_with_tols, model): + @property + def atol(self): + return self._atol + + @atol.setter + def atol(self, variables_with_tols, model): """ A method to set the absolute tolerances in the solver by state variable. + This method modifies self._atol. Parameters ---------- @@ -82,7 +88,7 @@ def set_tolerances_by_variable(self, variables_with_tols, model): def set_state_vec_tol(self, state_vec, tol): """ A method to set the tolerances in the atol vector of a specific - state variable. + state variable. This method modifies self._atol Parameters ---------- diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 6d442e12d0..39bfef6c99 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -76,6 +76,20 @@ def alg(t, y): true_solution = 0.1 * solution.t np.testing.assert_array_almost_equal(solution.y[0, :], true_solution) + def test_set_atol(self): + model = pybamm.lithium_ion.SPMe() + geometry = model.default_geometry + param = model.default_parameter_values + param.process_model(model) + param.process_geometry(geometry) + mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) + disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + disc.process_model(model) + solver = pybamm.IDAKLUSolver() + + variable_tols = {"Electrolyte concentration": 1e-3} + solver.set_tolerances_by_variable(variable_tols, model) + if __name__ == "__main__": print("Add -v for more debug output") From 6a759f1899133fdad9cfe9e9ab4f4847576b37c6 Mon Sep 17 00:00:00 2001 From: Scott Marquis Date: Mon, 4 Nov 2019 15:43:02 +0000 Subject: [PATCH 101/122] #697 changed back from asetter so can still do atol = const --- pybamm/solvers/c_solvers/idaklu.cpp | 2 +- pybamm/solvers/idaklu_solver.py | 7 +------ tests/integration/test_solvers/test_idaklu.py | 2 +- tests/unit/test_solvers/test_idaklu_solver.py | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 776e3a1240..55704e2161 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -365,7 +365,7 @@ PYBIND11_MODULE(idaklu, m) py::arg("yp0"), py::arg("res"), py::arg("jac"), py::arg("get_jac_data"), py::arg("get_jac_row_vals"), py::arg("get_jac_col_ptr"), py::arg("nnz"), py::arg("events"), py::arg("number_of_events"), py::arg("use_jacobian"), - py::arg("rhs_alg_id"), py::arg("rtol"), py::arg("atol"), + py::arg("rhs_alg_id"), py::arg("atol"), py::arg("rtol"), py::return_value_policy::take_ownership); py::class_(m, "solution") diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 3ac5873469..716e20b70c 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -45,12 +45,7 @@ def __init__( super().__init__("ida", rtol, atol, root_method, root_tol, max_steps) self.name = "IDA KLU solver" - @property - def atol(self): - return self._atol - - @atol.setter - def atol(self, variables_with_tols, model): + def set_atol_by_variable(self, variables_with_tols, model): """ A method to set the absolute tolerances in the solver by state variable. This method modifies self._atol. diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index ad83a7620e..b8fcca1c34 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -32,7 +32,7 @@ def test_set_tol_by_variable(self): solver = pybamm.IDAKLUSolver() variable_tols = {"Electrolyte concentration": 1e-3} - solver.set_tolerances_by_variable(variable_tols, model) + solver.set_atol_by_variable(variable_tols, model) solver.solve(model, t_eval) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 39bfef6c99..6d7977a8b7 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -88,7 +88,7 @@ def test_set_atol(self): solver = pybamm.IDAKLUSolver() variable_tols = {"Electrolyte concentration": 1e-3} - solver.set_tolerances_by_variable(variable_tols, model) + solver.set_atol_by_variable(variable_tols, model) if __name__ == "__main__": From 95fddc12d1fb18523ae3ff24b76d9ca59d5cb37b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 4 Nov 2019 16:46:23 +0000 Subject: [PATCH 102/122] #591 fix entropic change bug --- CHANGELOG.md | 1 + .../anodes/graphite_mcmb2528_Marquis2019/parameters.csv | 1 - .../lithium-ion/cathodes/lico2_Marquis2019/parameters.csv | 1 - .../electrolytes/lipf6_Marquis2019/parameters.csv | 3 +-- pybamm/parameters/standard_parameters_lithium_ion.py | 6 ++++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be17aca65..6fdd8a73a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ## Bug fixes +- Pass the correct dimensional temperature to entropic change functions ([]) - Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) diff --git a/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv b/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv index 614e516fb1..28a1733cb4 100644 --- a/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv @@ -4,7 +4,6 @@ Name [units],Value,Reference,Notes # Electrode properties,,, Negative electrode conductivity [S.m-1],100,Scott Moura FastDFN,graphite Maximum concentration in negative electrode [mol.m-3],24983.2619938437,Scott Moura FastDFN, -Negative electrode diffusion coefficient [m2.s-1],3.9E-14,Scott Moura FastDFN, Negative electrode diffusivity [m2.s-1],[function]graphite_mcmb2528_diffusivity_Dualfoil1998,, Negative electrode OCP [V],[function]graphite_mcmb2528_ocp_Dualfoil1998, ,,, diff --git a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv index f79e4fc401..99f2b3840b 100644 --- a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv @@ -4,7 +4,6 @@ Name [units],Value,Reference,Notes # Electrode properties,,, Positive electrode conductivity [S.m-1],10,Scott Moura FastDFN,lithium cobalt oxide Maximum concentration in positive electrode [mol.m-3],51217.9257309275,Scott Moura FastDFN, -Positive electrode diffusion coefficient [m2.s-1],1E-13,Scott Moura FastDFN, Positive electrode diffusivity [m2.s-1],[function]lico2_diffusivity_Dualfoil1998,, Positive electrode OCP [V],[function]lico2_ocp_Dualfoil1998, ,,, diff --git a/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv b/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv index d99621a3ac..c77aca897b 100644 --- a/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv @@ -4,9 +4,8 @@ Name [units],Value,Reference,Notes # Electrolyte properties,,, Typical electrolyte concentration [mol.m-3],1000,Scott Moura FastDFN, Cation transference number,0.4,Scott Moura FastDFN, -Typical lithium ion diffusivity [m2.s-1],5.34E-10,Scott Moura FastDFN, Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Capiglia1999,, -Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Capiglia1999,, +Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Capiglia1999,, ,,, # Activation energies,,, Reference temperature [K],298.15,25C, diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index e981b597fd..73a9f4b793 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -406,13 +406,15 @@ def m_p(T): def U_n(c_s_n, T): "Dimensionless open-circuit potential in the negative electrode" sto = c_s_n - return (U_n_dimensional(sto, T) - U_n_ref) / potential_scale + T_dim = Delta_T * T + T_ref + return (U_n_dimensional(sto, T_dim) - U_n_ref) / potential_scale def U_p(c_s_p, T): "Dimensionless open-circuit potential in the positive electrode" sto = c_s_p - return (U_p_dimensional(sto, T) - U_p_ref) / potential_scale + T_dim = Delta_T * T + T_ref + return (U_p_dimensional(sto, T_dim) - U_p_ref) / potential_scale def dUdT_n(c_s_n): From 29367f462b8db1acc33d7c940f1aeea755b017f6 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 4 Nov 2019 16:49:36 +0000 Subject: [PATCH 103/122] #591 update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fdd8a73a8..06f9cd7143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ ## Bug fixes -- Pass the correct dimensional temperature to entropic change functions ([]) +- Pass the correct dimensional temperature to entropic change functions ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) - Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) From f35543268327a16c8223aafa5035ec9b18bc33cd Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Mon, 4 Nov 2019 16:52:16 +0000 Subject: [PATCH 104/122] #591 update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f9cd7143..dde65eed1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ ## Bug fixes -- Pass the correct dimensional temperature to entropic change functions ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) +- Pass the correct dimensional temperature to open circuit potential ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) - Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) From 764ff2e3e71bc06614c5fffad4b75c5065739e02 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Mon, 4 Nov 2019 23:07:32 -0500 Subject: [PATCH 105/122] #685 set up casadi interpolation --- CHANGELOG.md | 2 +- examples/notebooks/change-input-current.ipynb | 16 +++++++------- .../1C_discharge_from_full/parameters.csv | 2 +- .../parameters.csv | 2 +- pybamm/__init__.py | 4 ++-- pybamm/expression_tree/functions.py | 2 +- pybamm/expression_tree/interpolant.py | 2 ++ .../operations/convert_to_casadi.py | 7 ++++++- pybamm/parameters/parameter_values.py | 4 ++-- .../get_constant_current.py | 2 +- .../get_user_current.py | 2 +- pybamm/solvers/dae_solver.py | 6 +++--- pybamm/solvers/ode_solver.py | 4 ++-- .../2019_08_sulzer_thesis/self_discharge.py | 2 +- .../drive_cycles/car_current_simulation.py | 2 +- results/drive_cycles/discharge_rest.py | 2 +- .../user_sin_current_simulation.py | 4 ++-- .../test_lead_acid/test_loqs.py | 2 +- .../test_composite_side_reactions.py | 2 +- .../test_full_side_reactions.py | 2 +- .../test_loqs_side_reactions.py | 2 +- .../test_lithium_ion/test_spm.py | 2 +- tests/integration/test_quick_plot.py | 2 +- .../test_operations/test_convert_to_casadi.py | 21 +++++++++++++++++++ .../test_parameters/test_current_functions.py | 8 +++---- .../test_electrical_parameters.py | 2 +- .../test_standard_parameters_lead_acid.py | 2 +- .../test_parameters/test_update_parameters.py | 2 +- tests/unit/test_solvers/test_casadi_solver.py | 6 +++--- 29 files changed, 73 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be17aca65..9ef48364a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Features - Added Simulation class ([#693](https://github.com/pybamm-team/PyBaMM/pull/693)) -- Add interface to CasADi solver ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) +- Add interface to CasADi solver ([#687](https://github.com/pybamm-team/PyBaMM/pull/687), [#691](https://github.com/pybamm-team/PyBaMM/pull/691)) - Add option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) - Add method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) - Add `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) diff --git a/examples/notebooks/change-input-current.ipynb b/examples/notebooks/change-input-current.ipynb index 7a1f0f7e9b..8226dabc84 100644 --- a/examples/notebooks/change-input-current.ipynb +++ b/examples/notebooks/change-input-current.ipynb @@ -22,7 +22,7 @@ "\n", "In this notebook we will use the SPM as the example model, and change the input current from the default option. If you are not familiar with running a model in PyBaMM, please see [this](./models/SPM.ipynb) notebook for more details.\n", "\n", - "In PyBaMM, the current function is set using the parameter \"Current function\". By default this is set to be a constant current provided by the class [`pybamm.GetConstantCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_constant_current.html). This class takes a single optional argument \"current\" which is the size of the current in Amperes. If no argument is passed, the value of the current is set by the parameter \"Typical current [A]\" (see [this](parameter-values.ipynb) notebook for more information about parameters).\n", + "In PyBaMM, the current function is set using the parameter \"Current function\". By default this is set to be a constant current provided by the class [`pybamm.ConstantCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_constant_current.html). This class takes a single optional argument \"current\" which is the size of the current in Amperes. If no argument is passed, the value of the current is set by the parameter \"Typical current [A]\" (see [this](parameter-values.ipynb) notebook for more information about parameters).\n", "\n", "In general it is recommended to change the size of a constant current input by changing the parameter \"Typical current [A]\", since this value is used to non-dimensionlise the model. Below we load the SPM with the default parameters, and then change the the typical current 16A. We then explicilty set the current function to be a constant current." ] @@ -49,14 +49,14 @@ "\n", "# change the typical current and set a constant discharge using the typical current value\n", "param[\"Typical current [A]\"] = 16\n", - "param[\"Current function\"] = pybamm.GetConstantCurrent()\n" + "param[\"Current function\"] = pybamm.ConstantCurrent()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "However, you may wish to change the value of the constant current without altering the typical current value used for non-dimensionlidation. For instance, the current function could be change to draw a current of 8A by updating the current function and passing the optional argument \"current\" to the class `GetConstantCurrent`" + "However, you may wish to change the value of the constant current without altering the typical current value used for non-dimensionlidation. For instance, the current function could be change to draw a current of 8A by updating the current function and passing the optional argument \"current\" to the class `ConstantCurrent`" ] }, { @@ -66,7 +66,7 @@ "outputs": [], "source": [ "# update the value of the current profile *without* changing the typical current used for non-dimensionlisation\n", - "param[\"Current function\"] = pybamm.GetConstantCurrent(current=pybamm.Scalar(8))" + "param[\"Current function\"] = pybamm.ConstantCurrent(current=pybamm.Scalar(8))" ] }, { @@ -240,7 +240,7 @@ "source": [ "## Adding your own current function \n", "\n", - "A user defined current function can be passed to any model using the class [`pybamm.GetUserCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_user_current). The class takes in a method, which returns the current as a function of time, followed by any keyword arguments required by the method. \n", + "A user defined current function can be passed to any model using the class [`pybamm.UserCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_user_current). The class takes in a method, which returns the current as a function of time, followed by any keyword arguments required by the method. \n", "\n", "For example, you may want to simulate a sinusoidal current with amplitude A and freqency omega. In order to do so you must first define the method" ] @@ -262,7 +262,7 @@ "source": [ "Note that time *must* me the first arguemnt of the function. The parameters may either be provided as floats, or may be one of the standard parameters provided in PyBaMM (see [here](https://pybamm.readthedocs.io/en/latest/source/parameters)). \n", "\n", - "The the model may be loaded and the \"Current function\" parameter updated to be a `GetUserCurrent` class which calls `my_fun`" + "The the model may be loaded and the \"Current function\" parameter updated to be a `UserCurrent` class which calls `my_fun`" ] }, { @@ -282,7 +282,7 @@ "# set user defined current function\n", "A = pybamm.electrical_parameters.I_typ\n", "omega = 0.1\n", - "param[\"Current function\"] = pybamm.GetUserCurrent(my_fun, A=A, omega=omega)\n", + "param[\"Current function\"] = pybamm.UserCurrent(my_fun, A=A, omega=omega)\n", "\n", "# process model and geometry\n", "param.process_model(model)\n", @@ -293,7 +293,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that the parameters in `my_fun` were passed as keyword arguments to the `GetUserCurrent` class. The model may then be solved in the usual way" + "Note that the parameters in `my_fun` were passed as keyword arguments to the `UserCurrent` class. The model may then be solved in the usual way" ] }, { diff --git a/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv b/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv index a69a021238..15dc8b80fa 100644 --- a/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv +++ b/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv @@ -11,7 +11,7 @@ Number of cells connected in series to make a battery,6,Manufacturer, Lower voltage cut-off [V],1.73,,(just under) 10.5V across 6-cell battery Upper voltage cut-off [V],2.44,,(just over) 14.5V across 6-cell battery C-rate,1,, -Current function,[inbuilt class]GetConstantCurrent,, +Current function,[inbuilt class]ConstantCurrent,, ,,, # Initial conditions Initial State of Charge,1,-, diff --git a/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv index dab0b3b48a..7c390e804e 100644 --- a/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv @@ -11,7 +11,7 @@ Number of cells connected in series to make a battery,1,, Lower voltage cut-off [V],3.105,, Upper voltage cut-off [V],4.7,, C-rate,1,, -Current function,[inbuilt class]GetConstantCurrent,, +Current function,[inbuilt class]ConstantCurrent,, ,,, # Initial conditions Initial concentration in negative electrode [mol.m-3],19986.609595075,Scott Moura FastDFN, diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 69cda501f7..4c72045389 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -273,9 +273,9 @@ def version(formatted=False): # from .parameters.standard_current_functions.base_current import GetCurrent from .parameters.standard_current_functions.get_constant_current import ( - GetConstantCurrent, + ConstantCurrent, ) -from .parameters.standard_current_functions.get_user_current import GetUserCurrent +from .parameters.standard_current_functions.get_user_current import UserCurrent from .parameters.standard_current_functions.get_current_data import GetCurrentData # diff --git a/pybamm/expression_tree/functions.py b/pybamm/expression_tree/functions.py index 1f1d28e524..bd46013eeb 100644 --- a/pybamm/expression_tree/functions.py +++ b/pybamm/expression_tree/functions.py @@ -230,7 +230,7 @@ def _function_simplify(self, simplified_children): if self.takes_no_params is True: # If self.function() takes no parameters then we can always simplify it return pybamm.Scalar(self.function()) - elif isinstance(self.function, pybamm.GetConstantCurrent): + elif isinstance(self.function, pybamm.ConstantCurrent): # If self.function() is a constant current then simplify to scalar return pybamm.Scalar(self.function.parameters_eval["Current [A]"]) else: diff --git a/pybamm/expression_tree/interpolant.py b/pybamm/expression_tree/interpolant.py index 5cb8a9c6ca..ef3c9a54b8 100644 --- a/pybamm/expression_tree/interpolant.py +++ b/pybamm/expression_tree/interpolant.py @@ -60,5 +60,7 @@ def __init__( interpolating_function, child, name=name, derivative="derivative" ) # Store information as attributes + self.x = data[:, 0] + self.y = data[:, 1] self.interpolator = interpolator self.extrapolate = extrapolate diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index d613badead..497f10af67 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -4,6 +4,7 @@ import pybamm import casadi import numpy as np +from scipy.interpolate import PchipInterpolator, CubicSpline class CasadiConverter(object): @@ -74,13 +75,17 @@ def _convert(self, symbol, t, y): return casadi.mmax(*converted_children) elif symbol.function == np.abs: return casadi.fabs(*converted_children) + elif isinstance(symbol.function, (PchipInterpolator, CubicSpline)): + return casadi.interpolant("LUT", "bspline", [symbol.x], symbol.y)( + *converted_children + ) elif not isinstance( symbol.function, pybamm.GetCurrent ) and symbol.function.__name__.startswith("elementwise_grad_of_"): differentiating_child_idx = int(symbol.function.__name__[-1]) # Create dummy symbolic variables in order to differentiate using CasADi dummy_vars = [ - casadi.SX.sym("y_" + str(i)) for i in range(len(converted_children)) + casadi.MX.sym("y_" + str(i)) for i in range(len(converted_children)) ] func_diff = casadi.gradient( symbol.differentiated_function(*dummy_vars), diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 9880746c00..85b8457ff3 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -174,14 +174,14 @@ def check_and_update_parameter_values(self, values): raise ValueError( """ "C-rate" cannot be zero. A possible alternative is to set - "Current function" to `pybamm.GetConstantCurrent(current=0)` instead. + "Current function" to `pybamm.ConstantCurrent(current=0)` instead. """ ) if "Typical current [A]" in values and values["Typical current [A]"] == 0: raise ValueError( """ "Typical current [A]" cannot be zero. A possible alternative is to set - "Current function" to `pybamm.GetConstantCurrent(current=0)` instead. + "Current function" to `pybamm.ConstantCurrent(current=0)` instead. """ ) # If the capacity of the cell has been provided, make sure "C-rate" and current diff --git a/pybamm/parameters/standard_current_functions/get_constant_current.py b/pybamm/parameters/standard_current_functions/get_constant_current.py index 099cec05bf..a57203f7ac 100644 --- a/pybamm/parameters/standard_current_functions/get_constant_current.py +++ b/pybamm/parameters/standard_current_functions/get_constant_current.py @@ -4,7 +4,7 @@ import pybamm -class GetConstantCurrent(pybamm.GetCurrent): +class ConstantCurrent(pybamm.GetCurrent): """ Sets a constant input current for a simulation. diff --git a/pybamm/parameters/standard_current_functions/get_user_current.py b/pybamm/parameters/standard_current_functions/get_user_current.py index 0b1cc0b40f..24ef792b0d 100644 --- a/pybamm/parameters/standard_current_functions/get_user_current.py +++ b/pybamm/parameters/standard_current_functions/get_user_current.py @@ -4,7 +4,7 @@ import pybamm -class GetUserCurrent(pybamm.GetCurrent): +class UserCurrent(pybamm.GetCurrent): """ Sets a user-defined function as the input current for a simulation. diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 74001b7b9c..46f8bfb001 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -227,10 +227,10 @@ def set_up_casadi(self, model): initial_conditions """ # Convert model attributes to casadi - t_casadi = casadi.SX.sym("t") + t_casadi = casadi.MX.sym("t") y0 = model.concatenated_initial_conditions - y_diff = casadi.SX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) - y_alg = casadi.SX.sym( + y_diff = casadi.MX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) + y_alg = casadi.MX.sym( "y_alg", len(model.concatenated_algebraic.evaluate(0, y0)) ) y_casadi = casadi.vertcat(y_diff, y_alg) diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index a1655d745f..168bc0479a 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -176,8 +176,8 @@ def set_up_casadi(self, model): y0 = model.concatenated_initial_conditions[:, 0] - t_casadi = casadi.SX.sym("t") - y_casadi = casadi.SX.sym("y", len(y0)) + t_casadi = casadi.MX.sym("t") + y_casadi = casadi.MX.sym("y", len(y0)) pybamm.logger.info("Converting RHS to CasADi") concatenated_rhs = model.concatenated_rhs.to_casadi(t_casadi, y_casadi) pybamm.logger.info("Converting events to CasADi") diff --git a/results/2019_08_sulzer_thesis/self_discharge.py b/results/2019_08_sulzer_thesis/self_discharge.py index 3b284d7b00..a040101832 100644 --- a/results/2019_08_sulzer_thesis/self_discharge.py +++ b/results/2019_08_sulzer_thesis/self_discharge.py @@ -32,7 +32,7 @@ def self_discharge_states(compute): ), ] extra_parameter_values = { - "Current function": pybamm.GetConstantCurrent(current=0) + "Current function": pybamm.ConstantCurrent(current=0) } t_eval = np.linspace(0, 1000, 100) all_variables, t_eval = model_comparison( diff --git a/results/drive_cycles/car_current_simulation.py b/results/drive_cycles/car_current_simulation.py index 81570e1bef..bcedf38408 100644 --- a/results/drive_cycles/car_current_simulation.py +++ b/results/drive_cycles/car_current_simulation.py @@ -40,7 +40,7 @@ def car_current(t): # load parameter values and process model and geometry param = model.default_parameter_values -param["Current function"] = pybamm.GetUserCurrent(car_current) +param["Current function"] = pybamm.UserCurrent(car_current) param.process_model(model) param.process_geometry(geometry) diff --git a/results/drive_cycles/discharge_rest.py b/results/drive_cycles/discharge_rest.py index 07f9007957..a576252285 100644 --- a/results/drive_cycles/discharge_rest.py +++ b/results/drive_cycles/discharge_rest.py @@ -41,7 +41,7 @@ # solve again with zero current, using last step of solution1 as initial conditions # update the current to be zero -param["Current function"] = pybamm.GetConstantCurrent(current=pybamm.Scalar(0)) +param["Current function"] = pybamm.ConstantCurrent(current=pybamm.Scalar(0)) param.update_model(model, disc) # Note: need to update model.concatenated_initial_conditions *after* update_model, # as update_model updates model.concatenated_initial_conditions, by concatenting diff --git a/results/drive_cycles/user_sin_current_simulation.py b/results/drive_cycles/user_sin_current_simulation.py index a2401c481e..ce579dd265 100644 --- a/results/drive_cycles/user_sin_current_simulation.py +++ b/results/drive_cycles/user_sin_current_simulation.py @@ -23,9 +23,9 @@ def my_fun(t, A, omega): # load parameter values and process models param = models[0].default_parameter_values for i, frequency in enumerate(frequencies): - # pass my_fun to GetUserCurrent class, giving the additonal parameters as + # pass my_fun to UserCurrent class, giving the additonal parameters as # keyword arguments - current = pybamm.GetUserCurrent(my_fun, A=A, omega=frequency) + current = pybamm.UserCurrent(my_fun, A=A, omega=frequency) param.update({"Current function": current}) param.process_model(models[i]) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index ce1f3e8e21..cc04f85dbb 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -47,7 +47,7 @@ def test_zero_current(self): model = pybamm.lead_acid.LOQS() parameter_values = model.default_parameter_values parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} + {"Current function": pybamm.ConstantCurrent(current=0)} ) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py index af855e849d..e172dcd3d1 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py @@ -37,7 +37,7 @@ def test_basic_processing_zero_current(self): model = pybamm.lead_acid.Composite(options) parameter_values = model.default_parameter_values parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} + {"Current function": pybamm.ConstantCurrent(current=0)} ) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py index eb4823a1d7..0478e4c07d 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py @@ -44,7 +44,7 @@ def test_basic_processing_zero_current(self): model = pybamm.lead_acid.Full(options) parameter_values = model.default_parameter_values parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} + {"Current function": pybamm.ConstantCurrent(current=0)} ) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py index 1139a7d129..4c6a15c511 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py @@ -45,7 +45,7 @@ def test_zero_current(self): model = pybamm.lead_acid.LOQS(options) parameter_values = model.default_parameter_values parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} + {"Current function": pybamm.ConstantCurrent(current=0)} ) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 0aed23ae2e..d9e8f696e5 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -84,7 +84,7 @@ def test_zero_current(self): model = pybamm.lithium_ion.SPM(options) parameter_values = model.default_parameter_values parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} + {"Current function": pybamm.ConstantCurrent(current=0)} ) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() diff --git a/tests/integration/test_quick_plot.py b/tests/integration/test_quick_plot.py index d197b94a6b..8863d5b89e 100644 --- a/tests/integration/test_quick_plot.py +++ b/tests/integration/test_quick_plot.py @@ -43,7 +43,7 @@ def test_plot_lithium_ion(self): quick_plot.update(0.01) # Update parameters, solve, plot again - param.update({"Current function": pybamm.GetConstantCurrent(current=0)}) + param.update({"Current function": pybamm.ConstantCurrent(current=0)}) param.update_model(spm, disc_spm) solution_spm = spm.default_solver.solve(spm, t_eval) quick_plot = pybamm.QuickPlot(spm, mesh, solution_spm) diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index a516cb7955..9ebc374672 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -81,6 +81,27 @@ def test_special_functions(self): self.assertEqual(pybamm.Function(np.abs, b).to_casadi(), casadi.SX(2)) self.assertEqual(pybamm.Function(np.abs, c).to_casadi(), casadi.SX(3)) + def test_interpolation(self): + x = np.linspace(0, 1)[:, np.newaxis] + y = pybamm.StateVector(slice(0, 2)) + casadi_y = casadi.MX.sym("y", 2) + # linear + linear = np.hstack([x, 2 * x]) + y_test = np.array([0.4, 0.6]) + for interpolator in ["pchip", "cubic spline"]: + interp = pybamm.Interpolant(linear, y, interpolator=interpolator) + interp_casadi = interp.to_casadi(y=casadi_y) + f = casadi.Function("f", [casadi_y], [interp_casadi]) + np.testing.assert_array_almost_equal(interp.evaluate(y=y_test), f(y_test)) + # square + square = np.hstack([x, x ** 2]) + y = pybamm.StateVector(slice(0, 1)) + for interpolator in ["pchip", "cubic spline"]: + interp = pybamm.Interpolant(square, y, interpolator=interpolator) + interp_casadi = interp.to_casadi(y=casadi_y) + f = casadi.Function("f", [casadi_y], [interp_casadi]) + np.testing.assert_array_almost_equal(interp.evaluate(y=y_test), f(y_test)) + def test_concatenations(self): y = np.linspace(0, 1, 10)[:, np.newaxis] a = pybamm.Vector(y) diff --git a/tests/unit/test_parameters/test_current_functions.py b/tests/unit/test_parameters/test_current_functions.py index 0d6a2a248d..11e48cd00e 100644 --- a/tests/unit/test_parameters/test_current_functions.py +++ b/tests/unit/test_parameters/test_current_functions.py @@ -13,7 +13,7 @@ def test_base_current(self): self.assertEqual(function(10), 1) def test_constant_current(self): - function = pybamm.GetConstantCurrent(current=4) + function = pybamm.ConstantCurrent(current=4) assert isinstance(function(0), numbers.Number) assert isinstance(function(np.zeros(3)), numbers.Number) assert isinstance(function(np.zeros([3, 3])), numbers.Number) @@ -24,7 +24,7 @@ def test_constant_current(self): { "Typical current [A]": 2, "Typical timescale [s]": 1, - "Current function": pybamm.GetConstantCurrent(), + "Current function": pybamm.ConstantCurrent(), } ) processed_current = parameter_values.process_symbol(current) @@ -69,9 +69,9 @@ def my_fun(t, A, omega): A = pybamm.electrical_parameters.I_typ omega = 3 - # pass my_fun to GetUserCurrent class, giving the additonal parameters as + # pass my_fun to UserCurrent class, giving the additonal parameters as # keyword arguments - current = pybamm.GetUserCurrent(my_fun, A=A, omega=omega) + current = pybamm.UserCurrent(my_fun, A=A, omega=omega) # set and process parameters parameter_values = pybamm.ParameterValues( diff --git a/tests/unit/test_parameters/test_electrical_parameters.py b/tests/unit/test_parameters/test_electrical_parameters.py index 8f8f7cad79..4075d3e733 100644 --- a/tests/unit/test_parameters/test_electrical_parameters.py +++ b/tests/unit/test_parameters/test_electrical_parameters.py @@ -24,7 +24,7 @@ def test_current_functions(self): "Number of electrodes connected in parallel to make a cell": 8, "Typical current [A]": 2, "Typical timescale [s]": 60, - "Current function": pybamm.GetConstantCurrent(), + "Current function": pybamm.ConstantCurrent(), } ) dimensional_current_eval = parameter_values.process_symbol(dimensional_current) diff --git a/tests/unit/test_parameters/test_standard_parameters_lead_acid.py b/tests/unit/test_parameters/test_standard_parameters_lead_acid.py index 47b1fd16b6..6a311ab7cc 100644 --- a/tests/unit/test_parameters/test_standard_parameters_lead_acid.py +++ b/tests/unit/test_parameters/test_standard_parameters_lead_acid.py @@ -89,7 +89,7 @@ def test_current_functions(self): "Typical electrolyte concentration [mol.m-3]": 1, "Number of electrodes connected in parallel to make a cell": 8, "Typical current [A]": 2, - "Current function": pybamm.GetConstantCurrent(), + "Current function": pybamm.ConstantCurrent(), } ) dimensional_current_density_eval = parameter_values.process_symbol( diff --git a/tests/unit/test_parameters/test_update_parameters.py b/tests/unit/test_parameters/test_update_parameters.py index bab24a8f42..8f6794fcc3 100644 --- a/tests/unit/test_parameters/test_update_parameters.py +++ b/tests/unit/test_parameters/test_update_parameters.py @@ -75,7 +75,7 @@ def test_update_model(self): chemistry=pybamm.parameter_sets.Marquis2019 ) parameter_values_update.update( - {"Current function": pybamm.GetConstantCurrent(current=pybamm.Scalar(0))} + {"Current function": pybamm.ConstantCurrent(current=pybamm.Scalar(0))} ) modeltest3.test_update_parameters(parameter_values_update) modeltest3.test_solving(t_eval=t_eval) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 89a3f3a175..ca2769bda2 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -14,8 +14,8 @@ def test_integrate(self): # Constant solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") - y = casadi.SX.sym("y") - constant_growth = casadi.SX(0.5) + y = casadi.MX.sym("y") + constant_growth = casadi.MX(0.5) problem = {"x": y, "ode": constant_growth} y0 = np.array([0]) @@ -40,7 +40,7 @@ def test_integrate_failure(self): # Turn off warnings to ignore sqrt error warnings.simplefilter("ignore") - y = casadi.SX.sym("y") + y = casadi.MX.sym("y") sqrt_decay = -np.sqrt(y) y0 = np.array([1]) From 3e11efe5c587d17c7f59986d48be5d09fa763b05 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 5 Nov 2019 17:13:45 +0000 Subject: [PATCH 106/122] #705 fix sign error --- CHANGELOG.md | 1 + .../spatial_methods/scikit_finite_element.py | 4 +- .../test_scikit_finite_element.py | 37 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde65eed1e..fbd3c1b491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ## Bug fixes +- Correct a sign error in Dirichlet boundary conditions in the Finite Element Methid ([]()) - Pass the correct dimensional temperature to open circuit potential ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) - Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) diff --git a/pybamm/spatial_methods/scikit_finite_element.py b/pybamm/spatial_methods/scikit_finite_element.py index ddf501049e..5d8331e489 100644 --- a/pybamm/spatial_methods/scikit_finite_element.py +++ b/pybamm/spatial_methods/scikit_finite_element.py @@ -121,7 +121,7 @@ def unit_bc_load_form(v, dv, w): # set Dirichlet value at facets corresponding to tab neg_bc_load = np.zeros(mesh.npts) neg_bc_load[mesh.negative_tab_dofs] = 1 - boundary_load = boundary_load - neg_bc_value * pybamm.Vector(neg_bc_load) + boundary_load = boundary_load + neg_bc_value * pybamm.Vector(neg_bc_load) else: raise ValueError( "boundary condition must be Dirichlet or Neumann, not '{}'".format( @@ -138,7 +138,7 @@ def unit_bc_load_form(v, dv, w): # set Dirichlet value at facets corresponding to tab pos_bc_load = np.zeros(mesh.npts) pos_bc_load[mesh.positive_tab_dofs] = 1 - boundary_load = boundary_load - pos_bc_value * pybamm.Vector(pos_bc_load) + boundary_load = boundary_load + pos_bc_value * pybamm.Vector(pos_bc_load) else: raise ValueError( "boundary condition must be Dirichlet or Neumann, not '{}'".format( diff --git a/tests/unit/test_spatial_methods/test_scikit_finite_element.py b/tests/unit/test_spatial_methods/test_scikit_finite_element.py index 5a108e275d..bf9a93a535 100644 --- a/tests/unit/test_spatial_methods/test_scikit_finite_element.py +++ b/tests/unit/test_spatial_methods/test_scikit_finite_element.py @@ -461,6 +461,43 @@ def test_pure_neumann_poisson(self): u_exact = z ** 2 / 2 - 1 / 6 np.testing.assert_array_almost_equal(solution.y[:-1], u_exact, decimal=1) + def test_dirichlet_bcs(self): + # manufactured solution u = a*z^2 + b*z + c + model = pybamm.BaseModel() + a = 3 + b = 4 + c = 5 + u = pybamm.Variable("variable", domain="current collector") + model.algebraic = {u: -pybamm.laplacian(u) + pybamm.source(2 * a, u)} + # set boundary conditions ("negative tab" = bottom of unit square, + # "positive tab" = top of unit square, elsewhere normal derivative is zero) + model.boundary_conditions = { + u: { + "negative tab": (pybamm.Scalar(c), "Dirichlet"), + "positive tab": (pybamm.Scalar(a + b + c), "Dirichlet"), + } + } + # bad initial guess (on purpose) + model.initial_conditions = {u: pybamm.Scalar(1)} + model.variables = {"u": u} + # create discretisation + mesh = get_unit_2p1D_mesh_for_testing(ypts=8, zpts=32) + spatial_methods = { + "macroscale": pybamm.FiniteVolume, + "current collector": pybamm.ScikitFiniteElement, + } + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + # solve model + solver = pybamm.AlgebraicSolver() + solution = solver.solve(model) + + # indepdent of y, so just check values for one y + z = mesh["current collector"][0].edges["z"][:, np.newaxis] + u_exact = a * z ** 2 + b * z + c + np.testing.assert_array_almost_equal(solution.y[0 : len(z)], u_exact) + if __name__ == "__main__": print("Add -v for more debug output") From e4cf50e32af6490a8da9d227beeca38c573d8960 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 5 Nov 2019 17:17:05 +0000 Subject: [PATCH 107/122] #705 changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd3c1b491..ffd77754d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ ## Bug fixes -- Correct a sign error in Dirichlet boundary conditions in the Finite Element Methid ([]()) +- Correct a sign error in Dirichlet boundary conditions in the Finite Element Method ([#706](https://github.com/pybamm-team/PyBaMM/pull/706)) - Pass the correct dimensional temperature to open circuit potential ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) - Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) - Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) From e0a4661c394fe8e608ee05ac0d3dcf6a9492f543 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Tue, 5 Nov 2019 12:31:00 -0500 Subject: [PATCH 108/122] #685 reformat current data --- examples/notebooks/change-input-current.ipynb | 62 +- input/drive_cycles/US06.csv | 1204 ++++++++--------- input/drive_cycles/car_current.csv | 30 +- pybamm/__init__.py | 9 +- .../operations/convert_to_casadi.py | 2 +- pybamm/parameters/parameter_values.py | 42 +- .../base_current.py | 2 +- ...onstant_current.py => constant_current.py} | 4 +- .../get_current_data.py | 91 -- .../get_user_current.py | 4 +- .../user_current.py | 31 + results/drive_cycles/US06_simulation.py | 4 +- .../test_parameters/test_current_functions.py | 18 +- 13 files changed, 701 insertions(+), 802 deletions(-) rename pybamm/parameters/standard_current_functions/{get_constant_current.py => constant_current.py} (86%) delete mode 100644 pybamm/parameters/standard_current_functions/get_current_data.py create mode 100644 pybamm/parameters/standard_current_functions/user_current.py diff --git a/examples/notebooks/change-input-current.ipynb b/examples/notebooks/change-input-current.ipynb index 8226dabc84..b4c6305de5 100644 --- a/examples/notebooks/change-input-current.ipynb +++ b/examples/notebooks/change-input-current.ipynb @@ -84,7 +84,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "fc997d14787542f69f2cba123ce0b994", + "model_id": "b900a39a72704c88927058510b7913b2", "version_major": 2, "version_minor": 0 }, @@ -127,11 +127,7 @@ "source": [ "## Loading in current data \n", "\n", - "Data can be loaded in from a csv file using the class [`pybamm.GetCurrentData`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_current_data). Data should be given as a function of time (in seconds) and may be dimensional (in Amperes) or non-diemsnional (e.g. C-rate). Optionally, voltage data (in Volts) may be loaded in for convinient plotting and comparison. \n", - "\n", - "The input csv files should be stored in PyBaMM/input/drive_cycles and have the headings: \"time [s]\"; either \"current [A]\" for dimensional data, or \"current []\" for dimensionless data; and, optionally, \"voltage [V]\". \n", - "\n", - "To load in dimensional data we simply provide the filename and set the units to \"[A]\", e.g." + "Data can be loaded in from a csv file by specifying the path to that file and using the prefix \"[current data]\"." ] }, { @@ -140,54 +136,30 @@ "metadata": {}, "outputs": [], "source": [ - "param[\"Current function\"] = pybamm.GetCurrentData(\"US06.csv\", units=\"[A]\")" + "param[\"Current function\"] = \"[current data]US06\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "For non dimensional data, you also need to provide a current scale. For instance, if the data were C-rate then you would set the current scale to the 1C discharge current. If the 1C discharge current were 24 A and we had some C-rate data in the file car_current.csv, this would be loaded in as" + "As an example, we show how to solve the SPM using the US06 drive cycle" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, - "outputs": [], - "source": [ - "param[\"Current function\"] = pybamm.GetCurrentData(\"car_current.csv\", units=\"[]\", current_scale=24)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As an example, we how to solve the SPM using the US06 drive cycle" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/user/Documents/PyBaMM/pybamm/parameters/standard_current_functions/get_current_data.py:88: ModelWarning: Requested time (2387.404004088835) is outside of the data range [0, 600]\n", - " pybamm.ModelWarning,\n" - ] - }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3f4c99601ab8417ba2186987cb532600", + "model_id": "26e7b28104b34991b3ebac266dab4934", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.03900922998674932, step=0.001), Output()),…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=0.026526276390989537, step=0.001), Output())…" ] }, "metadata": {}, @@ -202,7 +174,7 @@ "\n", "# load parameter values and process model and geometry\n", "param = model.default_parameter_values\n", - "param[\"Current function\"] = pybamm.GetCurrentData(\"US06.csv\", units=\"[A]\")\n", + "param[\"Current function\"] = \"[current data]US06\"\n", "param.process_model(model)\n", "param.process_geometry(geometry)\n", "\n", @@ -247,7 +219,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -267,7 +239,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -298,26 +270,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/user/Documents/PyBaMM/venv/lib/python3.6/site-packages/ipykernel_launcher.py:12: DeprecationWarning: object of type cannot be safely interpreted as an integer.\n", - " if sys.path[0] == '':\n" - ] - }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "913efe4e3e9e4b37bad2ded9c71b975e", + "model_id": "629672e99152464dad01e57ff152347a", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.0019504614993374662, step=9.75230749668733…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=0.0013263138195494769, step=6.63156909774738…" ] }, "metadata": {}, @@ -372,7 +336,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.4" } }, "nbformat": 4, diff --git a/input/drive_cycles/US06.csv b/input/drive_cycles/US06.csv index 287fd07020..e534a5db0f 100644 --- a/input/drive_cycles/US06.csv +++ b/input/drive_cycles/US06.csv @@ -1,603 +1,603 @@ # Based on the US06 drive cycle, -current [A],time [s] -0.012859,0 -0.012859,1 -0.012859,2 -0.012859,3 -0.012859,4 -0.012859,5 -0.013668,6 -0.023524,7 -0.03126,8 -0.050602,9 -0.58049,10 -2.6467,11 -3.8424,12 -4.1121,13 --0.23677,14 -3.1954,15 -4.134,16 -2.8259,17 -2.5489,18 -2.6931,19 -2.3144,20 -3.5036,21 -2.7033,22 -1.057,23 --0.44999,24 --0.4451,25 --1.8319,26 --0.6912,27 --0.41631,28 -0.39479,29 -1.4094,30 --0.067283,31 -0.40082,32 --1.6765,33 --3.0094,34 --2.9658,35 --2.8139,36 --2.1029,37 --1.7886,38 --1.0515,39 --0.33154,40 --0.00027238,41 -0.012859,42 -0.012859,43 -0.012859,44 -0.012859,45 -0.012859,46 -0.012859,47 -0.012859,48 -0.026246,49 -1.4175,50 -2.3404,51 -1.9203,52 -2.8051,53 -4.2382,54 -4.2743,55 -2.8574,56 -4.2758,57 -4.9662,58 -4.8662,59 -3.8788,60 -2.521,61 -3.5651,62 -3.3848,63 -3.6869,64 -3.1176,65 -2.8487,66 -2.1836,67 -0.18726,68 -0.91501,69 -0.0053402,70 --0.37207,71 --0.37225,72 -0.33741,73 -1.0426,74 -0.33741,75 --0.37219,76 --0.96565,77 -0.13474,78 -0.46979,79 -0.46799,80 -0.80472,81 -0.80717,82 -1.4932,83 -2.2074,84 -3.3239,85 -4.1708,86 -4.5251,87 -4.3083,88 -4.2568,89 -4.8156,90 -5.2028,91 -4.5034,92 -4.6358,93 -3.3841,94 -1.5935,95 -1.3703,96 -0.22134,97 --2.5008,98 --1.8485,99 --1.6727,100 --1.0737,101 --1.065,102 --1.4701,103 --1.7157,104 --1.5508,105 --1.6473,106 --1.3644,107 --1.5766,108 --1.4219,109 --1.4997,110 --1.242,111 --1.4213,112 --1.9789,113 --2.4495,114 --1.771,115 --0.67,116 --0.65407,117 --0.79789,118 --3.6676,119 --3.2481,120 --2.1471,121 --1.5972,122 --1.3075,123 --0.80837,124 --0.29408,125 --0.039143,126 --0.012101,127 --0.00027238,128 -0.012859,129 -0.012859,130 -0.012859,131 -0.012859,132 -0.012859,133 -0.012859,134 -0.012859,135 -0.13367,136 -1.3164,137 -2.9506,138 -4.3345,139 -5.7205,140 -5.4552,141 -5.8237,142 -6.0151,143 -3.7866,144 -2.7655,145 -2.3944,146 -2.948,147 -1.8924,148 -2.5999,149 -1.8149,150 -1.3287,151 -1.3425,152 -2.057,153 -0.67879,154 -0.67879,155 -2.0956,156 -3.7768,157 -1.8459,158 -3.0051,159 -1.3509,160 -3.1006,161 -1.4034,162 -1.4141,163 -1.4248,164 --0.50076,165 --1.2951,166 -0.40453,167 -0.21185,168 --0.11201,169 -1.1354,170 --0.62707,171 -1.4858,172 -0.011324,173 -0.55864,174 -0.18866,175 -1.1018,176 -1.1079,177 -0.37419,178 -0.18726,179 -0.5484,180 -0.54637,181 --0.86914,182 --0.24991,183 -1.3988,184 --2.4102,185 -5.0098,186 -2.5027,187 -3.1156,188 -2.6317,189 -0.96709,190 -3.2753,191 -1.7871,192 -2.998,193 -1.6541,194 -1.8721,195 -2.0978,196 -0.89478,197 -1.3038,198 --0.078976,199 -1.4977,200 -2.1259,201 -0.089321,202 -1.1053,203 -1.3141,204 -3.1926,205 -0.52137,206 -1.3521,207 -1.3591,208 -1.3661,209 -1.7966,210 -1.8134,211 -2.4758,212 -0.995,213 -1.8602,214 --2.6906,215 --0.067612,216 -1.5503,217 -1.3521,218 -0.10446,219 -2.1829,220 -0.73539,221 -0.31463,222 -1.5617,223 --0.49752,224 -2.1685,225 -0.7278,226 -1.1428,227 -0.51918,228 -1.3486,229 --0.35495,230 -1.124,231 -1.9596,232 -1.1428,233 -1.5655,234 -1.9998,235 -1.5964,236 --0.93064,237 -1.3556,238 --0.92625,239 -2.1543,240 --0.21363,241 -2.1543,242 -0.51263,243 --0.07259,244 -1.1146,245 -0.70538,246 -1.5276,247 -1.124,248 -0.7128,249 --0.49887,250 -0.89478,251 -1.7142,252 -0.70048,253 --0.21884,254 -1.2937,255 -0.68831,256 -0.88929,257 -1.093,258 -0.28185,259 -1.698,260 -1.5089,261 --0.078194,262 -0.88929,263 -1.093,264 --0.081297,265 -2.7118,266 -0.90306,267 --0.078194,268 -1.9111,269 -0.084907,270 -0.28185,271 -0.47868,272 -0.67631,273 -1.4829,274 -0.88383,275 -0.88383,276 -0.6811,277 -1.694,278 -0.48492,279 -0.88656,280 -1.2937,281 -1.5051,282 -0.69559,283 -3.1671,284 -2.1829,285 -1.9998,286 -1.5964,287 -2.6805,288 -1.4233,289 -2.9549,290 -1.4636,291 -1.6914,292 -2.8152,293 -1.0654,294 -2.1852,295 -1.757,296 -1.7695,297 -6.4018,298 -2.1159,299 -7.9136,300 --2.9379,301 -0.26072,302 -1.1932,303 -0.72206,304 -2.3667,305 -1.9156,306 -0.97617,307 --0.62823,308 -0.24381,309 -0.46769,310 -1.156,311 -0.69207,312 -1.3812,313 -1.1527,314 -1.6173,315 -3.2663,316 -2.3718,317 -3.5953,318 -4.151,319 -3.4975,320 -5.3167,321 -4.1756,322 -3.7387,323 --0.94696,324 --2.8522,325 -0.33794,326 -3.3004,327 -4.3626,328 -2.4058,329 -4.2276,330 -2.4839,331 -2.505,332 -1.4794,333 -2.5263,334 -1.4955,335 --0.40618,336 -1.4633,337 -0.42687,338 -0.41621,339 --0.94842,340 --0.95058,341 --2.8125,342 --1.9414,343 --2.0775,344 --4.2071,345 --1.091,346 -1.6593,347 --0.19359,348 -0.5667,349 -1.6394,350 -0.78484,351 -3.174,352 -1.6874,353 -3.0325,354 -1.0654,355 -1.9606,356 --0.023889,357 -0.83956,358 -0.83676,359 --0.63943,360 --0.34073,361 -0.35759,362 -0.13671,363 -1.6315,364 -1.8602,365 -0.14178,366 -1.6434,367 -2.5283,368 -2.1238,369 -1.9253,370 -1.4972,371 -1.9517,372 -1.9695,373 -0.41043,374 -2.8783,375 -1.1003,376 -1.1003,377 -2.4667,378 -2.0372,379 -1.5935,380 -1.8329,381 --0.15832,382 -1.8201,383 -0.44887,384 --0.16389,385 -1.5661,386 -1.5739,387 --0.0085663,388 -0.42601,389 -2.4615,390 -0.66037,391 -0.65518,392 -2.0145,393 -1.8031,394 -1.1263,395 -1.1263,396 -2.9716,397 -0.68939,398 -1.3775,399 -0.68671,400 -1.6054,401 -1.8458,402 -1.3922,403 -0.0053429,404 -2.074,405 -2.3266,406 -1.6454,407 -2.8354,408 -0.49679,409 -1.1966,410 -0.72483,411 -0.95438,412 -0.9513,413 -2.8354,414 -1.682,415 -2.1679,416 --0.95265,417 --0.14664,418 --0.15257,419 -0.00062616,420 -1.363,421 -2.0601,422 --0.31705,423 -2.9716,424 --0.47505,425 -1.3558,426 --1.1047,427 -0.41485,428 --0.17703,429 -1.2852,430 -0.84237,431 --0.029129,432 -0.16447,433 -1.4748,434 -1.2612,435 -1.0436,436 -0.16268,437 -0.59278,438 --0.34126,439 -1.6553,440 -0.1452,441 -1.4342,442 --0.046997,443 -1.4233,444 --0.19793,445 --4.0792,446 -0.88656,447 --0.50003,448 -0.26455,449 -1.4573,450 -0.26455,451 -1.4573,452 -0.86493,453 -1.67,454 -0.87569,455 -0.87569,456 -0.87569,457 -1.686,458 -0.076253,459 -1.686,460 -1.7021,461 -0.081997,462 -1.7021,463 -0.081997,464 -1.7021,465 -0.89753,466 -1.7183,467 -0.087843,468 -0.081997,469 --0.9152,470 --1.5834,471 --0.36787,472 --2.0567,473 --0.87672,474 --1.4815,475 --2.6241,476 --2.178,477 --0.036534,478 --0.90616,479 --2.4373,480 --2.9694,481 --3.5147,482 --1.8457,483 --0.98341,484 --2.4483,485 --3.8294,486 --1.9441,487 --0.048456,488 --1.0641,489 --1.0035,490 --0.66563,491 --0.32669,492 --0.027036,493 -0.012859,494 -0.012859,495 -0.012859,496 -0.012859,497 -0.012859,498 -0.012859,499 -0.012859,500 -0.013668,501 -0.34446,502 -1.4125,503 -2.4117,504 -3.237,505 -3.4134,506 -2.4722,507 -0.71203,508 --0.68697,509 --0.9946,510 --1.1241,511 --2.2309,512 --1.7637,513 --0.84058,514 -0.20754,515 -1.1729,516 -1.9691,517 -2.7371,518 -3.2787,519 -2.6509,520 -1.5214,521 --0.0058875,522 --1.3985,523 --1.9241,524 --1.808,525 --1.6276,526 --0.95015,527 --0.20711,528 --0.2798,529 --0.026526,530 -0.059693,531 -0.71684,532 -1.6183,533 -2.4505,534 -3.2363,535 -3.4634,536 -1.8132,537 -0.52817,538 --0.37846,539 --1.398,540 --1.9658,541 --1.8804,542 --1.5482,543 --0.64195,544 --0.15352,545 --0.0033494,546 -1.1079,547 -1.9138,548 -2.325,549 -3.0374,550 -3.3647,551 -3.7312,552 -0.086116,553 --2.2017,554 --2.3495,555 --2.0339,556 --1.4035,557 --0.91224,558 --0.37758,559 --0.056562,560 -0.012859,561 -0.012859,562 -0.012859,563 -0.012859,564 -0.012859,565 -0.012859,566 -0.012859,567 -0.014561,568 -0.70309,569 -2.0434,570 -3.5113,571 -3.4062,572 -3.9559,573 -7.1727,574 -3.4216,575 -3.9417,576 -6.9749,577 -8.1,578 -3.415,579 --0.024227,580 --0.25555,581 --0.3689,582 --1.9022,583 --2.9537,584 --2.95,585 --2.7289,586 --3.271,587 --3.5216,588 --2.3332,589 --2.2084,590 --2.0047,591 --1.3769,592 --0.34366,593 --0.035978,594 -0.012859,595 -0.012859,596 -0.012859,597 -0.012859,598 -0.012859,599 -0.012859,600 +# time [s],current [A] +0,0.012859 +1,0.012859 +2,0.012859 +3,0.012859 +4,0.012859 +5,0.012859 +6,0.013668 +7,0.023524 +8,0.03126 +9,0.050602 +10,0.58049 +11,2.6467 +12,3.8424 +13,4.1121 +14,-0.23677 +15,3.1954 +16,4.134 +17,2.8259 +18,2.5489 +19,2.6931 +20,2.3144 +21,3.5036 +22,2.7033 +23,1.057 +24,-0.44999 +25,-0.4451 +26,-1.8319 +27,-0.6912 +28,-0.41631 +29,0.39479 +30,1.4094 +31,-0.067283 +32,0.40082 +33,-1.6765 +34,-3.0094 +35,-2.9658 +36,-2.8139 +37,-2.1029 +38,-1.7886 +39,-1.0515 +40,-0.33154 +41,-0.00027238 +42,0.012859 +43,0.012859 +44,0.012859 +45,0.012859 +46,0.012859 +47,0.012859 +48,0.012859 +49,0.026246 +50,1.4175 +51,2.3404 +52,1.9203 +53,2.8051 +54,4.2382 +55,4.2743 +56,2.8574 +57,4.2758 +58,4.9662 +59,4.8662 +60,3.8788 +61,2.521 +62,3.5651 +63,3.3848 +64,3.6869 +65,3.1176 +66,2.8487 +67,2.1836 +68,0.18726 +69,0.91501 +70,0.0053402 +71,-0.37207 +72,-0.37225 +73,0.33741 +74,1.0426 +75,0.33741 +76,-0.37219 +77,-0.96565 +78,0.13474 +79,0.46979 +80,0.46799 +81,0.80472 +82,0.80717 +83,1.4932 +84,2.2074 +85,3.3239 +86,4.1708 +87,4.5251 +88,4.3083 +89,4.2568 +90,4.8156 +91,5.2028 +92,4.5034 +93,4.6358 +94,3.3841 +95,1.5935 +96,1.3703 +97,0.22134 +98,-2.5008 +99,-1.8485 +100,-1.6727 +101,-1.0737 +102,-1.065 +103,-1.4701 +104,-1.7157 +105,-1.5508 +106,-1.6473 +107,-1.3644 +108,-1.5766 +109,-1.4219 +110,-1.4997 +111,-1.242 +112,-1.4213 +113,-1.9789 +114,-2.4495 +115,-1.771 +116,-0.67 +117,-0.65407 +118,-0.79789 +119,-3.6676 +120,-3.2481 +121,-2.1471 +122,-1.5972 +123,-1.3075 +124,-0.80837 +125,-0.29408 +126,-0.039143 +127,-0.012101 +128,-0.00027238 +129,0.012859 +130,0.012859 +131,0.012859 +132,0.012859 +133,0.012859 +134,0.012859 +135,0.012859 +136,0.13367 +137,1.3164 +138,2.9506 +139,4.3345 +140,5.7205 +141,5.4552 +142,5.8237 +143,6.0151 +144,3.7866 +145,2.7655 +146,2.3944 +147,2.948 +148,1.8924 +149,2.5999 +150,1.8149 +151,1.3287 +152,1.3425 +153,2.057 +154,0.67879 +155,0.67879 +156,2.0956 +157,3.7768 +158,1.8459 +159,3.0051 +160,1.3509 +161,3.1006 +162,1.4034 +163,1.4141 +164,1.4248 +165,-0.50076 +166,-1.2951 +167,0.40453 +168,0.21185 +169,-0.11201 +170,1.1354 +171,-0.62707 +172,1.4858 +173,0.011324 +174,0.55864 +175,0.18866 +176,1.1018 +177,1.1079 +178,0.37419 +179,0.18726 +180,0.5484 +181,0.54637 +182,-0.86914 +183,-0.24991 +184,1.3988 +185,-2.4102 +186,5.0098 +187,2.5027 +188,3.1156 +189,2.6317 +190,0.96709 +191,3.2753 +192,1.7871 +193,2.998 +194,1.6541 +195,1.8721 +196,2.0978 +197,0.89478 +198,1.3038 +199,-0.078976 +200,1.4977 +201,2.1259 +202,0.089321 +203,1.1053 +204,1.3141 +205,3.1926 +206,0.52137 +207,1.3521 +208,1.3591 +209,1.3661 +210,1.7966 +211,1.8134 +212,2.4758 +213,0.995 +214,1.8602 +215,-2.6906 +216,-0.067612 +217,1.5503 +218,1.3521 +219,0.10446 +220,2.1829 +221,0.73539 +222,0.31463 +223,1.5617 +224,-0.49752 +225,2.1685 +226,0.7278 +227,1.1428 +228,0.51918 +229,1.3486 +230,-0.35495 +231,1.124 +232,1.9596 +233,1.1428 +234,1.5655 +235,1.9998 +236,1.5964 +237,-0.93064 +238,1.3556 +239,-0.92625 +240,2.1543 +241,-0.21363 +242,2.1543 +243,0.51263 +244,-0.07259 +245,1.1146 +246,0.70538 +247,1.5276 +248,1.124 +249,0.7128 +250,-0.49887 +251,0.89478 +252,1.7142 +253,0.70048 +254,-0.21884 +255,1.2937 +256,0.68831 +257,0.88929 +258,1.093 +259,0.28185 +260,1.698 +261,1.5089 +262,-0.078194 +263,0.88929 +264,1.093 +265,-0.081297 +266,2.7118 +267,0.90306 +268,-0.078194 +269,1.9111 +270,0.084907 +271,0.28185 +272,0.47868 +273,0.67631 +274,1.4829 +275,0.88383 +276,0.88383 +277,0.6811 +278,1.694 +279,0.48492 +280,0.88656 +281,1.2937 +282,1.5051 +283,0.69559 +284,3.1671 +285,2.1829 +286,1.9998 +287,1.5964 +288,2.6805 +289,1.4233 +290,2.9549 +291,1.4636 +292,1.6914 +293,2.8152 +294,1.0654 +295,2.1852 +296,1.757 +297,1.7695 +298,6.4018 +299,2.1159 +300,7.9136 +301,-2.9379 +302,0.26072 +303,1.1932 +304,0.72206 +305,2.3667 +306,1.9156 +307,0.97617 +308,-0.62823 +309,0.24381 +310,0.46769 +311,1.156 +312,0.69207 +313,1.3812 +314,1.1527 +315,1.6173 +316,3.2663 +317,2.3718 +318,3.5953 +319,4.151 +320,3.4975 +321,5.3167 +322,4.1756 +323,3.7387 +324,-0.94696 +325,-2.8522 +326,0.33794 +327,3.3004 +328,4.3626 +329,2.4058 +330,4.2276 +331,2.4839 +332,2.505 +333,1.4794 +334,2.5263 +335,1.4955 +336,-0.40618 +337,1.4633 +338,0.42687 +339,0.41621 +340,-0.94842 +341,-0.95058 +342,-2.8125 +343,-1.9414 +344,-2.0775 +345,-4.2071 +346,-1.091 +347,1.6593 +348,-0.19359 +349,0.5667 +350,1.6394 +351,0.78484 +352,3.174 +353,1.6874 +354,3.0325 +355,1.0654 +356,1.9606 +357,-0.023889 +358,0.83956 +359,0.83676 +360,-0.63943 +361,-0.34073 +362,0.35759 +363,0.13671 +364,1.6315 +365,1.8602 +366,0.14178 +367,1.6434 +368,2.5283 +369,2.1238 +370,1.9253 +371,1.4972 +372,1.9517 +373,1.9695 +374,0.41043 +375,2.8783 +376,1.1003 +377,1.1003 +378,2.4667 +379,2.0372 +380,1.5935 +381,1.8329 +382,-0.15832 +383,1.8201 +384,0.44887 +385,-0.16389 +386,1.5661 +387,1.5739 +388,-0.0085663 +389,0.42601 +390,2.4615 +391,0.66037 +392,0.65518 +393,2.0145 +394,1.8031 +395,1.1263 +396,1.1263 +397,2.9716 +398,0.68939 +399,1.3775 +400,0.68671 +401,1.6054 +402,1.8458 +403,1.3922 +404,0.0053429 +405,2.074 +406,2.3266 +407,1.6454 +408,2.8354 +409,0.49679 +410,1.1966 +411,0.72483 +412,0.95438 +413,0.9513 +414,2.8354 +415,1.682 +416,2.1679 +417,-0.95265 +418,-0.14664 +419,-0.15257 +420,0.00062616 +421,1.363 +422,2.0601 +423,-0.31705 +424,2.9716 +425,-0.47505 +426,1.3558 +427,-1.1047 +428,0.41485 +429,-0.17703 +430,1.2852 +431,0.84237 +432,-0.029129 +433,0.16447 +434,1.4748 +435,1.2612 +436,1.0436 +437,0.16268 +438,0.59278 +439,-0.34126 +440,1.6553 +441,0.1452 +442,1.4342 +443,-0.046997 +444,1.4233 +445,-0.19793 +446,-4.0792 +447,0.88656 +448,-0.50003 +449,0.26455 +450,1.4573 +451,0.26455 +452,1.4573 +453,0.86493 +454,1.67 +455,0.87569 +456,0.87569 +457,0.87569 +458,1.686 +459,0.076253 +460,1.686 +461,1.7021 +462,0.081997 +463,1.7021 +464,0.081997 +465,1.7021 +466,0.89753 +467,1.7183 +468,0.087843 +469,0.081997 +470,-0.9152 +471,-1.5834 +472,-0.36787 +473,-2.0567 +474,-0.87672 +475,-1.4815 +476,-2.6241 +477,-2.178 +478,-0.036534 +479,-0.90616 +480,-2.4373 +481,-2.9694 +482,-3.5147 +483,-1.8457 +484,-0.98341 +485,-2.4483 +486,-3.8294 +487,-1.9441 +488,-0.048456 +489,-1.0641 +490,-1.0035 +491,-0.66563 +492,-0.32669 +493,-0.027036 +494,0.012859 +495,0.012859 +496,0.012859 +497,0.012859 +498,0.012859 +499,0.012859 +500,0.012859 +501,0.013668 +502,0.34446 +503,1.4125 +504,2.4117 +505,3.237 +506,3.4134 +507,2.4722 +508,0.71203 +509,-0.68697 +510,-0.9946 +511,-1.1241 +512,-2.2309 +513,-1.7637 +514,-0.84058 +515,0.20754 +516,1.1729 +517,1.9691 +518,2.7371 +519,3.2787 +520,2.6509 +521,1.5214 +522,-0.0058875 +523,-1.3985 +524,-1.9241 +525,-1.808 +526,-1.6276 +527,-0.95015 +528,-0.20711 +529,-0.2798 +530,-0.026526 +531,0.059693 +532,0.71684 +533,1.6183 +534,2.4505 +535,3.2363 +536,3.4634 +537,1.8132 +538,0.52817 +539,-0.37846 +540,-1.398 +541,-1.9658 +542,-1.8804 +543,-1.5482 +544,-0.64195 +545,-0.15352 +546,-0.0033494 +547,1.1079 +548,1.9138 +549,2.325 +550,3.0374 +551,3.3647 +552,3.7312 +553,0.086116 +554,-2.2017 +555,-2.3495 +556,-2.0339 +557,-1.4035 +558,-0.91224 +559,-0.37758 +560,-0.056562 +561,0.012859 +562,0.012859 +563,0.012859 +564,0.012859 +565,0.012859 +566,0.012859 +567,0.012859 +568,0.014561 +569,0.70309 +570,2.0434 +571,3.5113 +572,3.4062 +573,3.9559 +574,7.1727 +575,3.4216 +576,3.9417 +577,6.9749 +578,8.1 +579,3.415 +580,-0.024227 +581,-0.25555 +582,-0.3689 +583,-1.9022 +584,-2.9537 +585,-2.95 +586,-2.7289 +587,-3.271 +588,-3.5216 +589,-2.3332 +590,-2.2084 +591,-2.0047 +592,-1.3769 +593,-0.34366 +594,-0.035978 +595,0.012859 +596,0.012859 +597,0.012859 +598,0.012859 +599,0.012859 +600,0.012859 diff --git a/input/drive_cycles/car_current.csv b/input/drive_cycles/car_current.csv index aaae1b7e5a..3d23ca51bc 100644 --- a/input/drive_cycles/car_current.csv +++ b/input/drive_cycles/car_current.csv @@ -1,16 +1,16 @@ # This is adapted from the file getCarCurrent.m which is part of the LIONSIMBA toolbox., -current [],time [s] -1,0 -1,50 --0.5,50.001 --0.5,60 -0.5,60.001 -0.5,210 -1,210.001 -1,410 -2,410.001 -2,415 -1.25,415.001 -1.25,615 --0.5,615.001 --0.5,3600 +# time [s], current [A] +0, 1 +50, 1 +50.001, -0.5 +60, -0.5 +60.001, 0.5 +210, 0.5 +210.001, 1 +410, 1 +410.001, 2 +415, 2 +415.001, 1.25 +615, 1.25 +615.001, -0.5 +3600, -0.5 diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 4c72045389..55ad930ce5 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -271,12 +271,9 @@ def version(formatted=False): # # Current profiles # -from .parameters.standard_current_functions.base_current import GetCurrent -from .parameters.standard_current_functions.get_constant_current import ( - ConstantCurrent, -) -from .parameters.standard_current_functions.get_user_current import UserCurrent -from .parameters.standard_current_functions.get_current_data import GetCurrentData +from .parameters.standard_current_functions.base_current import BaseCurrent +from .parameters.standard_current_functions.constant_current import ConstantCurrent +from .parameters.standard_current_functions.user_current import UserCurrent # # other diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 497f10af67..50335241e5 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -80,7 +80,7 @@ def _convert(self, symbol, t, y): *converted_children ) elif not isinstance( - symbol.function, pybamm.GetCurrent + symbol.function, pybamm.BaseCurrent ) and symbol.function.__name__.startswith("elementwise_grad_of_"): differentiating_child_idx = int(symbol.function.__name__[-1]) # Create dummy symbolic variables in order to differentiate using CasADi diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 85b8457ff3..e9826c2a23 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -4,7 +4,6 @@ import pybamm import pandas as pd import os -import numpy as np class ParameterValues(dict): @@ -126,6 +125,13 @@ def read_parameters_csv(self, filename): df.dropna(how="all", inplace=True) return {k: v for (k, v) in zip(df["Name [units]"], df["Value"])} + def __setitem__(self, key, value): + "Call the update functionality when doing a setitem" + self.update({key: value}) + + def setitemsuper(self, key, value): + super().__setitem__(key, value) + def update(self, values, check_conflict=False, path=""): # check parameter values values = self.check_and_update_parameter_values(values) @@ -155,16 +161,27 @@ def update(self, values, check_conflict=False, path=""): # Extra set of brackets at the end makes an instance of the # class self[name] = getattr(pybamm, value[15:])() - # Data is flagged with the string "[data]" - elif value.startswith("[data]"): - data = np.loadtxt(os.path.join(path, value[6:] + ".csv")) + # Data is flagged with the string "[data]" or "[current data]" + elif value.startswith("[current data]") or value.startswith( + "[data]" + ): + if value.startswith("[current data]"): + data_path = os.path.join( + pybamm.root_dir(), "input", "drive_cycles" + ) + filename = os.path.join(data_path, value[14:] + ".csv") + else: + filename = os.path.join(path, value[6:] + ".csv") + data = pd.read_csv( + filename, comment="#", skip_blank_lines=True + ).to_numpy() # Save name and data - self[name] = (value[6:], data) + self.setitemsuper(name, (value[6:], data)) # Anything else should be a converted to a float else: - self[name] = float(value) + self.setitemsuper(name, float(value)) else: - self[name] = value + self.setitemsuper(name, value) # reset processed symbols self._processed_symbols = {} @@ -409,16 +426,12 @@ def _process_symbol(self, symbol): # if current setter, process any parameters that are symbols and # store the evaluated symbol in the parameters_eval dict - if isinstance(function_name, pybamm.GetCurrent): + if isinstance(function_name, pybamm.BaseCurrent): for param, sym in function_name.parameters.items(): if isinstance(sym, pybamm.Symbol): new_sym = self.process_symbol(sym) function_name.parameters[param] = new_sym function_name.parameters_eval[param] = new_sym.evaluate() - # If loading data, need to update interpolant with - # evaluated parameters - if isinstance(function_name, pybamm.GetCurrentData): - function_name.interpolate() # Create Function or Interpolant objec if isinstance(function_name, tuple): @@ -500,7 +513,7 @@ def update_scalars(self, symbol): # KeyError -> name not in parameter dict, don't update continue elif isinstance(x, pybamm.Function): - if isinstance(x.function, pybamm.GetCurrent): + if isinstance(x.function, pybamm.BaseCurrent): # Need to update parameters dict to be that of the new current # function and make new parameters_eval dict to be processed x.function.parameters = self["Current function"].parameters @@ -518,9 +531,6 @@ def update_scalars(self, symbol): # KeyError -> name not in parameter dict, evaluate # unnamed Scalar x.function.parameters_eval[param] = new_sym.evaluate() - if isinstance(x.function, pybamm.GetCurrentData): - # update interpolant - x.function.interpolate() return symbol diff --git a/pybamm/parameters/standard_current_functions/base_current.py b/pybamm/parameters/standard_current_functions/base_current.py index 3eb6cfdf2c..2f239d00b7 100644 --- a/pybamm/parameters/standard_current_functions/base_current.py +++ b/pybamm/parameters/standard_current_functions/base_current.py @@ -3,7 +3,7 @@ # -class GetCurrent(object): +class BaseCurrent(object): """ The base class for setting the input current for a simulation. The parameters dictionary holds the symbols of any paramters required to evaluate the current. diff --git a/pybamm/parameters/standard_current_functions/get_constant_current.py b/pybamm/parameters/standard_current_functions/constant_current.py similarity index 86% rename from pybamm/parameters/standard_current_functions/get_constant_current.py rename to pybamm/parameters/standard_current_functions/constant_current.py index a57203f7ac..ccfdefbd73 100644 --- a/pybamm/parameters/standard_current_functions/get_constant_current.py +++ b/pybamm/parameters/standard_current_functions/constant_current.py @@ -4,7 +4,7 @@ import pybamm -class ConstantCurrent(pybamm.GetCurrent): +class ConstantCurrent(pybamm.BaseCurrent): """ Sets a constant input current for a simulation. @@ -13,7 +13,7 @@ class ConstantCurrent(pybamm.GetCurrent): current : :class:`pybamm.Symbol` or float The size of the current in Amperes. - **Extends:"": :class:`pybamm.GetCurrent` + **Extends:"": :class:`pybamm.BaseCurrent` """ def __init__(self, current=pybamm.electrical_parameters.I_typ): diff --git a/pybamm/parameters/standard_current_functions/get_current_data.py b/pybamm/parameters/standard_current_functions/get_current_data.py deleted file mode 100644 index d683120ea3..0000000000 --- a/pybamm/parameters/standard_current_functions/get_current_data.py +++ /dev/null @@ -1,91 +0,0 @@ -# -# Load current profile from a csv file -# -import pybamm -import os -import pandas as pd -import numpy as np -import warnings -import scipy.interpolate as interp - - -class GetCurrentData(pybamm.GetCurrent): - """ - A class which loads a current profile from a csv file and creates an - interpolating function which can be called during solve. - - Parameters - ---------- - filename : str - The name of the file to load. - units : str, optional - The units of the current data which is to be loaded. Can be "[]" for - dimenionless data (default), or "[A]" for current in Amperes. - current_scale : :class:`pybamm.Symbol` or float, optional - The scale the current in Amperes if loading non-dimensional data. Default - is to use the typical current I_typ - - **Extends:"": :class:`pybamm.GetCurrent` - """ - - def __init__( - self, filename, units="[]", current_scale=pybamm.electrical_parameters.I_typ - ): - self.parameters = {"Current [A]": current_scale} - self.parameters_eval = {"Current [A]": current_scale} - - # Load data from csv - if filename: - pybamm_path = pybamm.root_dir() - data = pd.read_csv( - os.path.join(pybamm_path, "input", "drive_cycles", filename), - comment="#", - skip_blank_lines=True, - ).to_dict("list") - - self.time = np.array(data["time [s]"]) - self.units = units - self.current = np.array(data["current " + units]) - # If voltage data is present, load it into the class - try: - self.voltage = np.array(data["voltage [V]"]) - except KeyError: - self.voltage = None - else: - raise pybamm.ModelError("No input file provided for current") - - def __str__(self): - return "Current from data" - - def interpolate(self): - " Creates the interpolant from the loaded data " - # If data is dimenionless, multiply by a typical current (e.g. data - # could be C-rate and current_scale the 1C discharge current). Otherwise, - # just import the current data. - if self.units == "[]": - current = self.parameters_eval["Current [A]"] * self.current - elif self.units == "[A]": - current = self.current - else: - raise pybamm.ModelError( - "Current data must have units [A] or be dimensionless" - ) - # Interpolate using Piecewise Cubic Hermite Interpolating Polynomial - # (does not overshoot non-smooth data) - self.current_interp = interp.PchipInterpolator(self.time, current) - - def __call__(self, t): - """ - Calls the interpolating function created using the data from user-supplied - data file at time t (seconds). - """ - - if np.min(t) < self.time[0] or np.max(t) > self.time[-1]: - warnings.warn( - "Requested time ({}) is outside of the data range [{}, {}]".format( - t, self.time[0], self.time[-1] - ), - pybamm.ModelWarning, - ) - - return self.current_interp(t) diff --git a/pybamm/parameters/standard_current_functions/get_user_current.py b/pybamm/parameters/standard_current_functions/get_user_current.py index 24ef792b0d..c4c01b13bc 100644 --- a/pybamm/parameters/standard_current_functions/get_user_current.py +++ b/pybamm/parameters/standard_current_functions/get_user_current.py @@ -4,7 +4,7 @@ import pybamm -class UserCurrent(pybamm.GetCurrent): +class UserCurrent(pybamm.BaseCurrent): """ Sets a user-defined function as the input current for a simulation. @@ -16,7 +16,7 @@ class UserCurrent(pybamm.GetCurrent): any keyword arguments, i.e. function(t, **kwargs). **kwargs : Any keyword arguments required by function. - **Extends:"": :class:`pybamm.GetCurrent` + **Extends:"": :class:`pybamm.BaseCurrent` """ def __init__(self, function, **kwargs): diff --git a/pybamm/parameters/standard_current_functions/user_current.py b/pybamm/parameters/standard_current_functions/user_current.py new file mode 100644 index 0000000000..c4c01b13bc --- /dev/null +++ b/pybamm/parameters/standard_current_functions/user_current.py @@ -0,0 +1,31 @@ +# +# Allow a user-defined current function +# +import pybamm + + +class UserCurrent(pybamm.BaseCurrent): + """ + Sets a user-defined function as the input current for a simulation. + + Parameters + ---------- + function : method + The method which returns the current (in Amperes) as a function of time + (in seconds). The first argument of function must be time, followed by + any keyword arguments, i.e. function(t, **kwargs). + **kwargs : Any keyword arguments required by function. + + **Extends:"": :class:`pybamm.BaseCurrent` + """ + + def __init__(self, function, **kwargs): + self.parameters = kwargs + self.parameters_eval = kwargs + self.function = function + + def __str__(self): + return "User defined current" + + def __call__(self, t): + return self.function(t, **self.parameters_eval) diff --git a/results/drive_cycles/US06_simulation.py b/results/drive_cycles/US06_simulation.py index 3f5ae2bb64..70edc7fef7 100644 --- a/results/drive_cycles/US06_simulation.py +++ b/results/drive_cycles/US06_simulation.py @@ -13,7 +13,7 @@ # load parameter values and process model and geometry param = model.default_parameter_values -param["Current function"] = pybamm.GetCurrentData("US06.csv", units="[A]") +param["Current function"] = "[current data]US06" param.process_model(model) param.process_geometry(geometry) @@ -31,7 +31,7 @@ t_eval = np.linspace(0, 600 / tau, 600) # need to increase max solver steps if solving DAEs along with an erratic drive cycle -solver = model.default_solver +solver = pybamm.CasadiSolver(mode="fast") # model.default_solver if isinstance(solver, pybamm.DaeSolver): solver.max_steps = 10000 diff --git a/tests/unit/test_parameters/test_current_functions.py b/tests/unit/test_parameters/test_current_functions.py index 11e48cd00e..0581511bb9 100644 --- a/tests/unit/test_parameters/test_current_functions.py +++ b/tests/unit/test_parameters/test_current_functions.py @@ -9,7 +9,7 @@ class TestCurrentFunctions(unittest.TestCase): def test_base_current(self): - function = pybamm.GetCurrent() + function = pybamm.BaseCurrent() self.assertEqual(function(10), 1) def test_constant_current(self): @@ -31,23 +31,13 @@ def test_constant_current(self): self.assertIsInstance(processed_current.simplify(), pybamm.Scalar) def test_get_current_data(self): - # test units - function_list = [ - pybamm.GetCurrentData("US06.csv", units="[A]"), - pybamm.GetCurrentData("car_current.csv", units="[]", current_scale=10), - ] - for function in function_list: - function.interpolate() - # test process parameters dimensional_current = pybamm.electrical_parameters.dimensional_current_with_time parameter_values = pybamm.ParameterValues( { "Typical current [A]": 2, "Typical timescale [s]": 1, - "Current function": pybamm.GetCurrentData( - "car_current.csv", units="[]" - ), + "Current function": "[current data]car_current", } ) dimensional_current_eval = parameter_values.process_symbol(dimensional_current) @@ -55,9 +45,7 @@ def test_get_current_data(self): def current(t): return dimensional_current_eval.evaluate(t=t) - function_list.append(current) - - standard_tests = StandardCurrentFunctionTests(function_list, always_array=True) + standard_tests = StandardCurrentFunctionTests([current], always_array=True) standard_tests.test_all() def test_user_current(self): From 57404c37d050078c36ae67fef0df23c039d5b876 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Tue, 5 Nov 2019 12:41:39 -0500 Subject: [PATCH 109/122] #685 change defaults to IDAKLU or casadi --- pybamm/models/base_model.py | 2 +- pybamm/models/full_battery_models/base_battery_model.py | 2 +- pybamm/models/full_battery_models/lead_acid/full.py | 4 +++- pybamm/models/full_battery_models/lead_acid/higher_order.py | 5 ++++- pybamm/models/full_battery_models/lead_acid/loqs.py | 5 ++++- pybamm/models/full_battery_models/lithium_ion/dfn.py | 5 ++++- pybamm/models/full_battery_models/lithium_ion/spm.py | 5 ++++- pybamm/models/full_battery_models/lithium_ion/spme.py | 5 ++++- pybamm/models/submodels/electrode/base_electrode.py | 5 ++++- pybamm/models/submodels/electrode/ohm/base_ohm.py | 5 ++++- pybamm/models/submodels/electrode/ohm/full_ohm.py | 5 ++++- pybamm/models/submodels/electrode/ohm/leading_ohm.py | 5 ++++- pybamm/models/submodels/electrode/ohm/surface_form_ohm.py | 5 ++++- .../conductivity/full_stefan_maxwell_conductivity.py | 5 ++++- 14 files changed, 49 insertions(+), 14 deletions(-) diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index 9d192af47a..c33f89b0c3 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -103,7 +103,7 @@ def __init__(self, name="Unnamed model"): # Default behaviour is to use the jacobian and simplify self.use_jacobian = True self.use_simplify = True - self.convert_to_format = "python" + self.convert_to_format = "casadi" def _set_dictionary(self, dict, name): """ diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index a0767a4573..e3e3e19559 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -139,7 +139,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScipySolver() + return pybamm.CasadiSolver() @property def options(self): diff --git a/pybamm/models/full_battery_models/lead_acid/full.py b/pybamm/models/full_battery_models/lead_acid/full.py index 5e13d911bf..576a79280f 100644 --- a/pybamm/models/full_battery_models/lead_acid/full.py +++ b/pybamm/models/full_battery_models/lead_acid/full.py @@ -134,5 +134,7 @@ def default_solver(self): and self.options["current collector"] == "uniform" ): return pybamm.ScipySolver() + elif pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() else: - return pybamm.ScikitsDaeSolver() + return pybamm.CasadiSolver() diff --git a/pybamm/models/full_battery_models/lead_acid/higher_order.py b/pybamm/models/full_battery_models/lead_acid/higher_order.py index 4f3e424d97..39cc87ceb9 100644 --- a/pybamm/models/full_battery_models/lead_acid/higher_order.py +++ b/pybamm/models/full_battery_models/lead_acid/higher_order.py @@ -176,7 +176,10 @@ def default_solver(self): self.options["current collector"] != "uniform" or self.options["surface form"] == "algebraic" ): - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() else: return pybamm.ScipySolver() diff --git a/pybamm/models/full_battery_models/lead_acid/loqs.py b/pybamm/models/full_battery_models/lead_acid/loqs.py index 8814d3268c..7de7050083 100644 --- a/pybamm/models/full_battery_models/lead_acid/loqs.py +++ b/pybamm/models/full_battery_models/lead_acid/loqs.py @@ -183,6 +183,9 @@ def default_solver(self): self.options["current collector"] != "uniform" or self.options["surface form"] == "algebraic" ): - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() else: return pybamm.ScipySolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index c448c048b6..bfdfc35c71 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -117,4 +117,7 @@ def default_solver(self): """ # Default solver to DAE - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index dcab9c6ada..f5ddfcd659 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -128,4 +128,7 @@ def default_solver(self): if dimensionality == 0: return pybamm.ScipySolver() else: - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py index e5e6167592..383eb67cb4 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spme.py +++ b/pybamm/models/full_battery_models/lithium_ion/spme.py @@ -126,4 +126,7 @@ def default_solver(self): if dimensionality == 0: return pybamm.ScipySolver() else: - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/pybamm/models/submodels/electrode/base_electrode.py index fc986a694d..86cd970404 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -148,4 +148,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/base_ohm.py b/pybamm/models/submodels/electrode/ohm/base_ohm.py index 780f2790f0..ec8361c0ea 100644 --- a/pybamm/models/submodels/electrode/ohm/base_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/base_ohm.py @@ -49,4 +49,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/full_ohm.py b/pybamm/models/submodels/electrode/ohm/full_ohm.py index 61a10911f4..30dbac2602 100644 --- a/pybamm/models/submodels/electrode/ohm/full_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/full_ohm.py @@ -108,4 +108,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 654aa8399b..9f99f26cdc 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -77,4 +77,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScikitsOdeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py b/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py index 6dad293054..61c773c8a9 100644 --- a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py @@ -72,4 +72,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py index f4fae2f790..c6f8f8ddb9 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py @@ -71,4 +71,7 @@ def default_solver(self): """ Create and return the default solver for this model """ - return pybamm.ScikitsDaeSolver() + if pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() From 53c0b56c7c2086648bda0926534f58cf9a177e5e Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Tue, 5 Nov 2019 12:52:09 -0500 Subject: [PATCH 110/122] #685 remove skips from tests as they should use casadi --- pybamm/solvers/idaklu_solver.py | 2 +- pybamm/solvers/scikits_ode_solver.py | 2 +- .../test_lead_acid/test_asymptotics_convergence.py | 1 - .../test_lead_acid/test_compare_outputs.py | 1 - .../test_lead_acid/test_composite.py | 1 - .../test_full_battery_models/test_lead_acid/test_foqs.py | 1 - .../test_full_battery_models/test_lead_acid/test_full.py | 5 ----- .../test_side_reactions/test_composite_side_reactions.py | 5 +---- .../test_side_reactions/test_full_side_reactions.py | 6 +----- .../test_side_reactions/test_loqs_side_reactions.py | 5 +---- .../test_full_battery_models/test_lithium_ion/test_dfn.py | 3 --- .../test_full_battery_models/test_lithium_ion/test_spm.py | 6 +----- .../test_full_battery_models/test_lithium_ion/test_spme.py | 2 -- tests/integration/test_quick_plot.py | 2 -- tests/integration/test_solvers/test_idaklu.py | 2 +- .../test_lead_acid/test_composite.py | 2 -- .../test_full_battery_models/test_lead_acid/test_full.py | 2 -- .../test_full_battery_models/test_lead_acid/test_loqs.py | 1 - .../test_full_battery_models/test_lithium_ion/test_dfn.py | 7 ------- .../test_full_battery_models/test_lithium_ion/test_spm.py | 7 ------- .../test_full_battery_models/test_lithium_ion/test_spme.py | 7 ------- tests/unit/test_solvers/test_idaklu_solver.py | 2 +- tests/unit/test_solvers/test_scikits_solvers.py | 2 +- 23 files changed, 9 insertions(+), 65 deletions(-) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 716e20b70c..e5b2ecdca1 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -14,7 +14,7 @@ def have_idaklu(): - return idaklu_spec is None + return idaklu_spec is not None class IDAKLUSolver(pybamm.DaeSolver): diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index 6fbe70127d..1f8b78b5e8 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -16,7 +16,7 @@ def have_scikits_odes(): - return scikits_odes_spec is None + return scikits_odes_spec is not None class ScikitsOdeSolver(pybamm.OdeSolver): diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py index 62cf64f8f1..7efc5b9729 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py @@ -8,7 +8,6 @@ class TestAsymptoticConvergence(unittest.TestCase): - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_leading_order_convergence(self): """ Check that the leading-order model solution converges linearly in C_e to the diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py index 5dbd216562..aa31ad8386 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py @@ -7,7 +7,6 @@ from tests import StandardOutputComparison -@unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") class TestCompareOutputs(unittest.TestCase): def test_compare_averages_asymptotics(self): """ diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 97feb0c297..1b96658772 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -55,7 +55,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"surface form": "algebraic"} model = pybamm.lead_acid.Composite(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py index 737a408dc6..8bb33e789e 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py @@ -64,7 +64,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = { "surface form": "algebraic", diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py index e09ec47eaa..6d05b7cc74 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -9,14 +9,12 @@ class TestLeadAcidFull(unittest.TestCase): - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.Full(options) modeltest = tests.StandardModelTest(model) modeltest.test_all(t_eval=np.linspace(0, 0.6)) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_with_convection(self): options = {"thermal": "isothermal", "convection": True} model = pybamm.lead_acid.Full(options) @@ -40,7 +38,6 @@ def test_optimisations(self): np.testing.assert_array_almost_equal(original, simp_and_known) np.testing.assert_array_almost_equal(original, simp_and_python) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_set_up(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.Full(options) @@ -58,7 +55,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"surface form": "algebraic"} model = pybamm.lead_acid.Full(options) @@ -78,7 +74,6 @@ def test_optimisations(self): np.testing.assert_array_almost_equal(original, using_known_evals) np.testing.assert_array_almost_equal(original, simp_and_known, decimal=5) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_set_up(self): options = {"surface form": "differential"} model = pybamm.lead_acid.Full(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py index e172dcd3d1..ab27afcc30 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py @@ -15,7 +15,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"side reactions": ["oxygen"], "surface form": "algebraic"} model = pybamm.lead_acid.Composite(options) @@ -36,9 +35,7 @@ def test_basic_processing_zero_current(self): options = {"side reactions": ["oxygen"], "surface form": "differential"} model = pybamm.lead_acid.Composite(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.ConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py index 0478e4c07d..7ea41e2e34 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py @@ -9,7 +9,6 @@ class TestLeadAcidFullSideReactions(unittest.TestCase): - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing(self): options = {"side reactions": ["oxygen"]} model = pybamm.lead_acid.Full(options) @@ -22,7 +21,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"side reactions": ["oxygen"], "surface form": "algebraic"} model = pybamm.lead_acid.Full(options) @@ -43,9 +41,7 @@ def test_basic_processing_zero_current(self): options = {"side reactions": ["oxygen"], "surface form": "differential"} model = pybamm.lead_acid.Full(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.ConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py index 4c6a15c511..50685e37b9 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py @@ -23,7 +23,6 @@ def test_discharge_differential_varying_surface_area(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_discharge_algebraic(self): options = {"surface form": "algebraic", "side reactions": ["oxygen"]} model = pybamm.lead_acid.LOQS(options) @@ -44,9 +43,7 @@ def test_zero_current(self): options = {"surface form": "differential", "side reactions": ["oxygen"]} model = pybamm.lead_acid.LOQS(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.ConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 9dd672ebec..b8ac651251 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -8,7 +8,6 @@ import unittest -@unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") class TestDFN(unittest.TestCase): def test_basic_processing(self): options = {"thermal": "isothermal"} @@ -18,7 +17,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.DFN(options) @@ -35,7 +33,6 @@ def test_basic_processing_1plus1D(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.DFN(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index d9e8f696e5..3f77fa6e85 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -14,7 +14,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.SPM(options) @@ -31,7 +30,6 @@ def test_basic_processing_1plus1D(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.SPM(options) @@ -83,9 +81,7 @@ def test_zero_current(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.SPM(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.ConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 46199f70d8..0dc59f881c 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -15,7 +15,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.SPMe(options) @@ -32,7 +31,6 @@ def test_basic_processing_1plus1D(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.SPMe(options) diff --git a/tests/integration/test_quick_plot.py b/tests/integration/test_quick_plot.py index 8863d5b89e..0ebc03d08b 100644 --- a/tests/integration/test_quick_plot.py +++ b/tests/integration/test_quick_plot.py @@ -8,7 +8,6 @@ class TestQuickPlot(unittest.TestCase): Tests that QuickPlot is created correctly """ - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_plot_lithium_ion(self): spm = pybamm.lithium_ion.SPM() spme = pybamm.lithium_ion.SPMe() @@ -73,7 +72,6 @@ def test_plot_lithium_ion(self): quick_plot.update(0.01) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_plot_lead_acid(self): loqs = pybamm.lead_acid.LOQS() geometry = loqs.default_geometry diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index b8fcca1c34..ac00023692 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -4,7 +4,7 @@ import unittest -@unittest.skipIf(pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(~pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_on_spme(self): model = pybamm.lithium_ion.SPMe() diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 33a86fc467..3e3c3b3190 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -22,7 +22,6 @@ def test_well_posed_differential(self): class TestLeadAcidCompositeMultiDimensional(unittest.TestCase): - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_well_posed(self): model = pybamm.lead_acid.Composite( {"dimensionality": 1, "current collector": "potential pair"} @@ -58,7 +57,6 @@ def test_well_posed_differential(self): model = pybamm.lead_acid.Composite(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_well_posed_algebraic(self): options = {"surface form": "algebraic", "side reactions": ["oxygen"]} model = pybamm.lead_acid.Composite(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py index 8683ec464c..785e7f6d12 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -15,7 +15,6 @@ def test_well_posed_with_convection(self): model = pybamm.lead_acid.Full(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): model = pybamm.lead_acid.Full() self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) @@ -37,7 +36,6 @@ def test_well_posed_algebraic(self): model = pybamm.lead_acid.Full(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"surface form": "differential"} model = pybamm.lead_acid.Full(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index c4bbfd75a5..e48ad9f8be 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -146,7 +146,6 @@ def test_well_posed_1plus1D(self): model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"surface form": "differential"} model = pybamm.lead_acid.LOQS(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 3fb5480675..7e1384c4e6 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -48,7 +48,6 @@ def test_x_full_thermal_model_no_current_collector(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.DFN(options) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_full_Nplus1D_not_implemented(self): # 1plus1D options = { @@ -91,7 +90,6 @@ def test_x_lumped_thermal_model_0D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -109,7 +107,6 @@ def test_xyz_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -127,7 +124,6 @@ def test_xyz_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -137,7 +133,6 @@ def test_x_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -147,7 +142,6 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_set_temperature_1D(self): options = { "current collector": "potential pair", @@ -165,7 +159,6 @@ def test_x_lumped_thermal_set_temperature_1D(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.DFN(options) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.DFN(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 6600c68c9d..2ddefc7022 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -75,7 +75,6 @@ def test_x_full_thermal_model_no_current_collector(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPM(options) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_full_Nplus1D_not_implemented(self): # 1plus1D options = { @@ -118,7 +117,6 @@ def test_x_lumped_thermal_model_0D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -136,7 +134,6 @@ def test_xyz_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -154,7 +151,6 @@ def test_xyz_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -164,7 +160,6 @@ def test_x_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -174,7 +169,6 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_set_temperature_1D(self): options = { "current collector": "potential pair", @@ -192,7 +186,6 @@ def test_x_lumped_thermal_set_temperature_1D(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPM(options) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.SPM(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 53f2fee5a7..60ef953623 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -62,7 +62,6 @@ def test_x_full_thermal_model_no_current_collector(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPMe(options) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_full_Nplus1D_not_implemented(self): # 1plus1D options = { @@ -105,7 +104,6 @@ def test_x_lumped_thermal_model_0D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -123,7 +121,6 @@ def test_xyz_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -133,7 +130,6 @@ def test_xyz_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -151,7 +147,6 @@ def test_x_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -161,7 +156,6 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_set_temperature_1D(self): options = { "current collector": "potential pair", @@ -179,7 +173,6 @@ def test_x_lumped_thermal_set_temperature_1D(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPMe(options) - @unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") def test_default_solver(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.SPMe(options) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 6d7977a8b7..4cf4f5af02 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -7,7 +7,7 @@ import unittest -@unittest.skipIf(pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(~pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index b264921202..f9c4e5395e 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -9,7 +9,7 @@ from tests import get_mesh_for_testing, get_discretisation_for_testing -@unittest.skipIf(pybamm.have_scikits_odes(), "scikits.odes not installed") +@unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") class TestScikitsSolvers(unittest.TestCase): def test_ode_integrate(self): # Constant From 3a2c03700a31fc8ccdf6a002a52865f3e328cea6 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 6 Nov 2019 10:45:02 +0000 Subject: [PATCH 111/122] #705 fix disc of spatial var --- .../spatial_methods/scikit_finite_element.py | 6 ++-- tests/unit/test_processed_variable.py | 28 +++++++------------ .../test_scikit_finite_element.py | 27 +++++++++++++++++- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/pybamm/spatial_methods/scikit_finite_element.py b/pybamm/spatial_methods/scikit_finite_element.py index 5d8331e489..f6e0832cec 100644 --- a/pybamm/spatial_methods/scikit_finite_element.py +++ b/pybamm/spatial_methods/scikit_finite_element.py @@ -51,11 +51,13 @@ def spatial_variable(self, symbol): symbol_mesh = self.mesh if symbol.name == "y": vector = pybamm.Vector( - symbol_mesh["current collector"][0].edges["y"], domain=symbol.domain + symbol_mesh["current collector"][0].coordinates[0, :][:, np.newaxis] + # symbol_mesh["current collector"][0].edges["y"], domain=symbol.domain ) elif symbol.name == "z": vector = pybamm.Vector( - symbol_mesh["current collector"][0].edges["z"], domain=symbol.domain + symbol_mesh["current collector"][0].coordinates[1, :][:, np.newaxis] + # symbol_mesh["current collector"][0].edges["z"], domain=symbol.domain ) else: raise pybamm.GeometryError( diff --git a/tests/unit/test_processed_variable.py b/tests/unit/test_processed_variable.py index a891fdbc6b..87c29a84a0 100644 --- a/tests/unit/test_processed_variable.py +++ b/tests/unit/test_processed_variable.py @@ -140,13 +140,11 @@ def test_processed_variable_3D_x_z(self): def test_processed_variable_3D_scikit(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y = disc.mesh["current collector"][0].edges["y"] + z = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) @@ -154,25 +152,23 @@ def test_processed_variable_3D_scikit(self): processed_var = pybamm.ProcessedVariable(var_sol, t_sol, u_sol, mesh=disc.mesh) np.testing.assert_array_equal( processed_var.entries, - np.reshape(u_sol, [len(y_sol), len(z_sol), len(t_sol)]), + np.reshape(u_sol, [len(y), len(z), len(t_sol)]), ) def test_processed_variable_2Dspace_scikit(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y = disc.mesh["current collector"][0].edges["y"] + z = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] processed_var = pybamm.ProcessedVariable(var_sol, t_sol, u_sol, mesh=disc.mesh) np.testing.assert_array_equal( - processed_var.entries, np.reshape(u_sol, [len(y_sol), len(z_sol)]) + processed_var.entries, np.reshape(u_sol, [len(y), len(z)]) ) def test_processed_var_1D_interpolation(self): @@ -367,13 +363,11 @@ def test_processed_var_3D_r_first_dimension(self): def test_processed_var_3D_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y_sol = disc.mesh["current collector"][0].edges["y"] + z_sol = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) @@ -406,13 +400,11 @@ def test_processed_var_3D_scikit_interpolation(self): def test_processed_var_2Dspace_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y_sol = disc.mesh["current collector"][0].edges["y"] + z_sol = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] diff --git a/tests/unit/test_spatial_methods/test_scikit_finite_element.py b/tests/unit/test_spatial_methods/test_scikit_finite_element.py index bf9a93a535..21bd4b8799 100644 --- a/tests/unit/test_spatial_methods/test_scikit_finite_element.py +++ b/tests/unit/test_spatial_methods/test_scikit_finite_element.py @@ -493,11 +493,36 @@ def test_dirichlet_bcs(self): solver = pybamm.AlgebraicSolver() solution = solver.solve(model) - # indepdent of y, so just check values for one y + # indepedent of y, so just check values for one y z = mesh["current collector"][0].edges["z"][:, np.newaxis] u_exact = a * z ** 2 + b * z + c np.testing.assert_array_almost_equal(solution.y[0 : len(z)], u_exact) + def test_disc_spatial_var(self): + mesh = get_unit_2p1D_mesh_for_testing(ypts=4, zpts=5) + spatial_methods = { + "macroscale": pybamm.FiniteVolume, + "current collector": pybamm.ScikitFiniteElement, + } + disc = pybamm.Discretisation(mesh, spatial_methods) + + # discretise y and z + y = pybamm.SpatialVariable("y", ["current collector"]) + z = pybamm.SpatialVariable("z", ["current collector"]) + y_disc = disc.process_symbol(y) + z_disc = disc.process_symbol(z) + + # create expected meshgrid + y_vec = np.linspace(0, 1, 4) + z_vec = np.linspace(0, 1, 5) + Y, Z = np.meshgrid(y_vec, z_vec) + y_actual = np.transpose(Y).flatten()[:, np.newaxis] + z_actual = np.transpose(Z).flatten()[:, np.newaxis] + + # spatial vars should discretise to the flattend meshgrid + np.testing.assert_array_equal(y_disc.evaluate(), y_actual) + np.testing.assert_array_equal(z_disc.evaluate(), z_actual) + if __name__ == "__main__": print("Add -v for more debug output") From cb19c95f4ac1626a35bad70496b5eabf6b4290ee Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 6 Nov 2019 12:52:29 +0000 Subject: [PATCH 112/122] #705 remove old spatial var disc --- pybamm/spatial_methods/scikit_finite_element.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pybamm/spatial_methods/scikit_finite_element.py b/pybamm/spatial_methods/scikit_finite_element.py index f6e0832cec..464a1d68a7 100644 --- a/pybamm/spatial_methods/scikit_finite_element.py +++ b/pybamm/spatial_methods/scikit_finite_element.py @@ -52,12 +52,10 @@ def spatial_variable(self, symbol): if symbol.name == "y": vector = pybamm.Vector( symbol_mesh["current collector"][0].coordinates[0, :][:, np.newaxis] - # symbol_mesh["current collector"][0].edges["y"], domain=symbol.domain ) elif symbol.name == "z": vector = pybamm.Vector( symbol_mesh["current collector"][0].coordinates[1, :][:, np.newaxis] - # symbol_mesh["current collector"][0].edges["z"], domain=symbol.domain ) else: raise pybamm.GeometryError( From f632714c9b06b14c119041a43c8920e1acc2ec3d Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 6 Nov 2019 08:44:28 -0500 Subject: [PATCH 113/122] #685 move default solver to base model --- pybamm/models/base_model.py | 11 +++++++++++ .../full_battery_models/base_battery_model.py | 7 ------- .../full_battery_models/lead_acid/full.py | 16 ---------------- .../lead_acid/higher_order.py | 17 ----------------- .../full_battery_models/lead_acid/loqs.py | 17 ----------------- .../full_battery_models/lithium_ion/dfn.py | 12 ------------ .../full_battery_models/lithium_ion/spm.py | 15 --------------- .../full_battery_models/lithium_ion/spme.py | 15 --------------- .../effective_resistance_current_collector.py | 4 ---- .../submodels/electrode/base_electrode.py | 10 ---------- .../models/submodels/electrode/ohm/base_ohm.py | 10 ---------- .../models/submodels/electrode/ohm/full_ohm.py | 10 ---------- .../submodels/electrode/ohm/leading_ohm.py | 10 ---------- .../submodels/electrode/ohm/surface_form_ohm.py | 10 ---------- .../full_stefan_maxwell_conductivity.py | 10 ---------- 15 files changed, 11 insertions(+), 163 deletions(-) diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index c33f89b0c3..a65a171725 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -479,3 +479,14 @@ def check_variables(self): var ) ) + + @property + def default_solver(self): + "Return default solver based on whether model is ODE model or DAE model" + if len(self.algebraic) == 0: + return pybamm.ScipySolver() + elif pybamm.have_idaklu(): + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver() + diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index e3e3e19559..a6b0a7779a 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -134,13 +134,6 @@ def default_spatial_methods(self): base_spatial_methods["current collector"] = pybamm.ScikitFiniteElement return base_spatial_methods - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.CasadiSolver() - @property def options(self): return self._options diff --git a/pybamm/models/full_battery_models/lead_acid/full.py b/pybamm/models/full_battery_models/lead_acid/full.py index 576a79280f..056a3efc31 100644 --- a/pybamm/models/full_battery_models/lead_acid/full.py +++ b/pybamm/models/full_battery_models/lead_acid/full.py @@ -122,19 +122,3 @@ def set_side_reaction_submodels(self): self.submodels[ "negative oxygen interface" ] = pybamm.interface.lead_acid_oxygen.NoReaction(self.param, "Negative") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - if ( - self.options["surface form"] == "differential" - and self.options["current collector"] == "uniform" - ): - return pybamm.ScipySolver() - elif pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/full_battery_models/lead_acid/higher_order.py b/pybamm/models/full_battery_models/lead_acid/higher_order.py index 39cc87ceb9..a03afdc51b 100644 --- a/pybamm/models/full_battery_models/lead_acid/higher_order.py +++ b/pybamm/models/full_battery_models/lead_acid/higher_order.py @@ -166,23 +166,6 @@ def set_full_porosity_submodel(self): """ self.submodels["full porosity"] = pybamm.porosity.Full(self.param) - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - if ( - self.options["current collector"] != "uniform" - or self.options["surface form"] == "algebraic" - ): - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() - else: - return pybamm.ScipySolver() - class FOQS(BaseHigherOrderModel): """First-order quasi-static model for lead-acid, from [1]_. diff --git a/pybamm/models/full_battery_models/lead_acid/loqs.py b/pybamm/models/full_battery_models/lead_acid/loqs.py index 7de7050083..cf6bea3ed1 100644 --- a/pybamm/models/full_battery_models/lead_acid/loqs.py +++ b/pybamm/models/full_battery_models/lead_acid/loqs.py @@ -172,20 +172,3 @@ def set_side_reaction_submodels(self): self.reaction_submodels["Positive"].append( self.submodels["leading-order positive oxygen interface"] ) - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - - if ( - self.options["current collector"] != "uniform" - or self.options["surface form"] == "algebraic" - ): - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() - else: - return pybamm.ScipySolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index bfdfc35c71..6268229624 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -109,15 +109,3 @@ def default_geometry(self): return pybamm.Geometry("1+1D macro", "(1+1)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+1)+1D micro") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - - # Default solver to DAE - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index f5ddfcd659..d11f38abe1 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -117,18 +117,3 @@ def default_geometry(self): return pybamm.Geometry("1+1D macro", "(1+0)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+0)+1D micro") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - dimensionality = self.options["dimensionality"] - if dimensionality == 0: - return pybamm.ScipySolver() - else: - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py index 383eb67cb4..974b60759d 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spme.py +++ b/pybamm/models/full_battery_models/lithium_ion/spme.py @@ -115,18 +115,3 @@ def default_geometry(self): return pybamm.Geometry("1+1D macro", "(1+0)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+0)+1D micro") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - dimensionality = self.options["dimensionality"] - if dimensionality == 0: - return pybamm.ScipySolver() - else: - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py index 793adeff00..a061994f12 100644 --- a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py +++ b/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py @@ -224,7 +224,3 @@ def default_submesh_types(self): @property def default_spatial_methods(self): return {"current collector": pybamm.ScikitFiniteElement} - - @property - def default_solver(self): - return pybamm.AlgebraicSolver() diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/pybamm/models/submodels/electrode/base_electrode.py index 86cd970404..146c8c916d 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -142,13 +142,3 @@ def _get_standard_whole_cell_variables(self, variables): variables = {"Electrode current density": i_s} return variables - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/base_ohm.py b/pybamm/models/submodels/electrode/ohm/base_ohm.py index ec8361c0ea..b2bb673e5a 100644 --- a/pybamm/models/submodels/electrode/ohm/base_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/base_ohm.py @@ -43,13 +43,3 @@ def set_boundary_conditions(self, variables): ) self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/full_ohm.py b/pybamm/models/submodels/electrode/ohm/full_ohm.py index 30dbac2602..fac16021bc 100644 --- a/pybamm/models/submodels/electrode/ohm/full_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/full_ohm.py @@ -102,13 +102,3 @@ def set_initial_conditions(self, variables): ) self.initial_conditions[phi_s] = phi_s_init - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 9f99f26cdc..2cc50a6e54 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -71,13 +71,3 @@ def set_boundary_conditions(self, variables): rbc = (pybamm.Scalar(0), "Neumann") self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py b/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py index 61c773c8a9..f78453e495 100644 --- a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py @@ -66,13 +66,3 @@ def get_coupled_variables(self, variables): variables.update(self._get_standard_whole_cell_variables(variables)) return variables - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py index c6f8f8ddb9..89272892c8 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py @@ -65,13 +65,3 @@ def set_initial_conditions(self, variables): phi_e = variables["Electrolyte potential"] T_ref = self.param.T_ref self.initial_conditions = {phi_e: -self.param.U_n(self.param.c_n_init, T_ref)} - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - if pybamm.have_idaklu(): - return pybamm.IDAKLUSolver() - else: - return pybamm.CasadiSolver() From cc33c9569012cd06f865d1e0a70d52805ad5500c Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 6 Nov 2019 08:51:48 -0500 Subject: [PATCH 114/122] #685 update (remove) solver tests --- .../effective_resistance_current_collector.py | 4 ++++ tests/unit/test_models/test_base_model.py | 7 +++++++ .../test_lead_acid/test_full.py | 12 ------------ .../test_lead_acid/test_loqs.py | 15 --------------- .../test_lithium_ion/test_dfn.py | 5 ----- .../test_lithium_ion/test_spm.py | 9 --------- .../test_lithium_ion/test_spme.py | 8 -------- 7 files changed, 11 insertions(+), 49 deletions(-) diff --git a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py index a061994f12..793adeff00 100644 --- a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py +++ b/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py @@ -224,3 +224,7 @@ def default_submesh_types(self): @property def default_spatial_methods(self): return {"current collector": pybamm.ScikitFiniteElement} + + @property + def default_solver(self): + return pybamm.AlgebraicSolver() diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 1e6e54579c..3978c06844 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -374,6 +374,13 @@ def test_default_solver(self): ) self.assertIsInstance(solver, pybamm.BaseModel) + # check that adding algebraic variables gives DAE solver + a = pybamm.Variable("a") + model.algebraic = {a: a - 1} + self.assertIsInstance( + model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) + ) + def test_default_parameters(self): # check parameters are read in ok model = pybamm.BaseBatteryModel() diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py index 785e7f6d12..a30b2c76f7 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -15,10 +15,6 @@ def test_well_posed_with_convection(self): model = pybamm.lead_acid.Full(options) model.check_well_posedness() - def test_default_solver(self): - model = pybamm.lead_acid.Full() - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - class TestLeadAcidFullSurfaceForm(unittest.TestCase): def test_well_posed_differential(self): @@ -36,14 +32,6 @@ def test_well_posed_algebraic(self): model = pybamm.lead_acid.Full(options) model.check_well_posedness() - def test_default_solver(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.Full(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - options = {"surface form": "algebraic"} - model = pybamm.lead_acid.Full(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - class TestLeadAcidFullSideReactions(unittest.TestCase): def test_well_posed(self): diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index e48ad9f8be..89e76f1801 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -146,21 +146,6 @@ def test_well_posed_1plus1D(self): model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - def test_default_solver(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.LOQS(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - options = { - "surface form": "differential", - "current collector": "potential pair", - "dimensionality": 1, - } - model = pybamm.lead_acid.LOQS(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - options = {"surface form": "algebraic"} - model = pybamm.lead_acid.LOQS(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - def test_default_geometry(self): options = {"surface form": "differential"} model = pybamm.lead_acid.LOQS(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 7e1384c4e6..4fcabbccb0 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -159,11 +159,6 @@ def test_x_lumped_thermal_set_temperature_1D(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.DFN(options) - def test_default_solver(self): - options = {"thermal": "isothermal"} - model = pybamm.lithium_ion.DFN(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - def test_particle_fast_diffusion(self): options = {"particle": "fast diffusion"} model = pybamm.lithium_ion.DFN(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 2ddefc7022..3115655336 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -186,15 +186,6 @@ def test_x_lumped_thermal_set_temperature_1D(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPM(options) - def test_default_solver(self): - options = {"thermal": "isothermal"} - model = pybamm.lithium_ion.SPM(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - - options = {"current collector": "potential pair", "dimensionality": 2} - model = pybamm.lithium_ion.SPM(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - def test_particle_fast_diffusion(self): options = {"particle": "fast diffusion"} model = pybamm.lithium_ion.SPM(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 60ef953623..8b73fdbffb 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -173,14 +173,6 @@ def test_x_lumped_thermal_set_temperature_1D(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPMe(options) - def test_default_solver(self): - options = {"thermal": "isothermal"} - model = pybamm.lithium_ion.SPMe(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - options = {"current collector": "potential pair", "dimensionality": 2} - model = pybamm.lithium_ion.SPMe(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - def test_particle_fast_diffusion(self): options = {"particle": "fast diffusion"} model = pybamm.lithium_ion.SPMe(options) From 52e09a8a23d53597d40fe8dc00a5a279e9bf38de Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 6 Nov 2019 08:54:52 -0500 Subject: [PATCH 115/122] #685 remove some slow tests --- .../test_lead_acid/test_composite.py | 24 -------------- .../test_lead_acid/test_foqs.py | 32 ------------------- 2 files changed, 56 deletions(-) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 1b96658772..1b81910416 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -63,30 +63,6 @@ def test_basic_processing_algebraic(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - def test_optimisations(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.Composite(options) - optimtest = tests.OptimisationsTest(model) - - original = optimtest.evaluate_model() - simplified = optimtest.evaluate_model(simplify=True) - using_known_evals = optimtest.evaluate_model(use_known_evals=True) - simp_and_known = optimtest.evaluate_model(simplify=True, use_known_evals=True) - simp_and_python = optimtest.evaluate_model(simplify=True, to_python=True) - np.testing.assert_array_almost_equal(original, simplified) - np.testing.assert_array_almost_equal(original, using_known_evals) - np.testing.assert_array_almost_equal(original, simp_and_known) - np.testing.assert_array_almost_equal(original, simp_and_python) - - def test_set_up(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.Composite(options) - optimtest = tests.OptimisationsTest(model) - optimtest.set_up_model(simplify=False, to_python=True) - optimtest.set_up_model(simplify=True, to_python=True) - optimtest.set_up_model(simplify=False, to_python=False) - optimtest.set_up_model(simplify=True, to_python=False) - class TestLeadAcidCompositeExtended(unittest.TestCase): def test_basic_processing(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py index 8bb33e789e..be8c4cbca0 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py @@ -76,38 +76,6 @@ def test_basic_processing_algebraic(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - def test_optimisations(self): - options = { - "surface form": "differential", - "thermal": "isothermal", - "convection": False, - } - model = pybamm.lead_acid.FOQS(options) - optimtest = tests.OptimisationsTest(model) - - original = optimtest.evaluate_model() - simplified = optimtest.evaluate_model(simplify=True) - using_known_evals = optimtest.evaluate_model(use_known_evals=True) - simp_and_known = optimtest.evaluate_model(simplify=True, use_known_evals=True) - simp_and_python = optimtest.evaluate_model(simplify=True, to_python=True) - np.testing.assert_array_almost_equal(original, simplified) - np.testing.assert_array_almost_equal(original, using_known_evals) - np.testing.assert_array_almost_equal(original, simp_and_known) - np.testing.assert_array_almost_equal(original, simp_and_python) - - def test_set_up(self): - options = { - "surface form": "differential", - "thermal": "isothermal", - "convection": False, - } - model = pybamm.lead_acid.FOQS(options) - optimtest = tests.OptimisationsTest(model) - optimtest.set_up_model(simplify=False, to_python=True) - optimtest.set_up_model(simplify=True, to_python=True) - optimtest.set_up_model(simplify=False, to_python=False) - optimtest.set_up_model(simplify=True, to_python=False) - if __name__ == "__main__": print("Add -v for more debug output") From 1175cbf6a2e17d86c801cc0d5b04f235ead7297f Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Wed, 6 Nov 2019 22:01:07 -0500 Subject: [PATCH 116/122] #685 get drive cycle example working --- pybamm/expression_tree/interpolant.py | 15 +++++++++++++++ .../operations/convert_to_casadi.py | 2 +- pybamm/parameters/parameter_values.py | 4 +++- pybamm/solvers/casadi_solver.py | 1 + pybamm/solvers/dae_solver.py | 1 + results/drive_cycles/US06_simulation.py | 10 +++++----- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pybamm/expression_tree/interpolant.py b/pybamm/expression_tree/interpolant.py index ef3c9a54b8..d60f7b23c1 100644 --- a/pybamm/expression_tree/interpolant.py +++ b/pybamm/expression_tree/interpolant.py @@ -60,7 +60,22 @@ def __init__( interpolating_function, child, name=name, derivative="derivative" ) # Store information as attributes + self.data = data self.x = data[:, 0] self.y = data[:, 1] self.interpolator = interpolator self.extrapolate = extrapolate + + def _function_new_copy(self, children): + """ See :meth:`Function._function_new_copy()` """ + return pybamm.Interpolant( + self.data, + *children, + name=self.name, + interpolator=self.interpolator, + extrapolate=self.extrapolate, + ) + + def _function_simplify(self, simplified_children): + """ See :meth:`Function._function_new_simplify()` """ + return self._function_new_copy(simplified_children) diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 50335241e5..5deac9ce59 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -40,7 +40,7 @@ def convert(self, symbol, t=None, y=None): def _convert(self, symbol, t, y): """ See :meth:`CasadiConverter.convert()`. """ if isinstance(symbol, (pybamm.Scalar, pybamm.Array, pybamm.Time)): - return casadi.SX(symbol.evaluate(t, y)) + return casadi.MX(symbol.evaluate(t, y)) elif isinstance(symbol, pybamm.StateVector): if y is None: diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index e9826c2a23..fbc388bb95 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -170,13 +170,15 @@ def update(self, values, check_conflict=False, path=""): pybamm.root_dir(), "input", "drive_cycles" ) filename = os.path.join(data_path, value[14:] + ".csv") + function_name = value[14:] else: filename = os.path.join(path, value[6:] + ".csv") + function_name = value[6:] data = pd.read_csv( filename, comment="#", skip_blank_lines=True ).to_numpy() # Save name and data - self.setitemsuper(name, (value[6:], data)) + self.setitemsuper(name, (function_name, data)) # Anything else should be a converted to a float else: self.setitemsuper(name, float(value)) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index b31b5f32e8..c14f49802b 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -226,6 +226,7 @@ def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): "reltol": self.rtol, "abstol": self.atol, "output_t0": True, + "max_num_steps": self.max_steps, } options.update(self.extra_options) if self.method == "idas": diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 46f8bfb001..1ee69adfe7 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -329,6 +329,7 @@ def eval_event(t, y): # Create CasADi problem for the CasADi solver self.casadi_problem = { + "t": t_casadi, "x": y_diff, "z": y_alg, "ode": concatenated_rhs, diff --git a/results/drive_cycles/US06_simulation.py b/results/drive_cycles/US06_simulation.py index 70edc7fef7..64f47fd5e5 100644 --- a/results/drive_cycles/US06_simulation.py +++ b/results/drive_cycles/US06_simulation.py @@ -18,20 +18,20 @@ param.process_geometry(geometry) # set mesh -mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) # simulate US06 drive cycle -tau = param.process_symbol( - pybamm.standard_parameters_lithium_ion.tau_discharge -).evaluate(0) +tau = param.evaluate(pybamm.standard_parameters_lithium_ion.tau_discharge) t_eval = np.linspace(0, 600 / tau, 600) # need to increase max solver steps if solving DAEs along with an erratic drive cycle -solver = pybamm.CasadiSolver(mode="fast") # model.default_solver +solver = pybamm.CasadiSolver() if isinstance(solver, pybamm.DaeSolver): solver.max_steps = 10000 From 9911d4130711ac3c76edd0f861d2c7cfc5e057f6 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Thu, 7 Nov 2019 11:32:59 -0500 Subject: [PATCH 117/122] #685 fix casadi tests --- .../test_operations/test_convert_to_casadi.py | 70 +++++++++++-------- .../test_lead_acid/test_composite.py | 8 ++- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 9ebc374672..7f8287591e 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -10,52 +10,58 @@ class TestCasadiConverter(unittest.TestCase): + def assert_casadi_equal(self, a, b, evalf=False): + if evalf is True: + self.assertTrue((casadi.evalf(a) - casadi.evalf(b)).is_zero()) + else: + self.assertTrue((a - b).is_zero()) + def test_convert_scalar_symbols(self): a = pybamm.Scalar(0) b = pybamm.Scalar(1) c = pybamm.Scalar(-1) d = pybamm.Scalar(2) - self.assertEqual(a.to_casadi(), casadi.SX(0)) - self.assertEqual(d.to_casadi(), casadi.SX(2)) + self.assertEqual(a.to_casadi(), casadi.MX(0)) + self.assertEqual(d.to_casadi(), casadi.MX(2)) # negate - self.assertEqual((-b).to_casadi(), casadi.SX(-1)) + self.assertEqual((-b).to_casadi(), casadi.MX(-1)) # absolute value - self.assertEqual(abs(c).to_casadi(), casadi.SX(1)) + self.assertEqual(abs(c).to_casadi(), casadi.MX(1)) # function def sin(x): return np.sin(x) f = pybamm.Function(sin, b) - self.assertEqual(f.to_casadi(), casadi.SX(np.sin(1))) + self.assertEqual(f.to_casadi(), casadi.MX(np.sin(1))) def myfunction(x, y): return x + y f = pybamm.Function(myfunction, b, d) - self.assertEqual(f.to_casadi(), casadi.SX(3)) + self.assertEqual(f.to_casadi(), casadi.MX(3)) # addition - self.assertEqual((a + b).to_casadi(), casadi.SX(1)) + self.assertEqual((a + b).to_casadi(), casadi.MX(1)) # subtraction - self.assertEqual((c - d).to_casadi(), casadi.SX(-3)) + self.assertEqual((c - d).to_casadi(), casadi.MX(-3)) # multiplication - self.assertEqual((c * d).to_casadi(), casadi.SX(-2)) + self.assertEqual((c * d).to_casadi(), casadi.MX(-2)) # power - self.assertEqual((c ** d).to_casadi(), casadi.SX(1)) + self.assertEqual((c ** d).to_casadi(), casadi.MX(1)) # division - self.assertEqual((b / d).to_casadi(), casadi.SX(1 / 2)) + self.assertEqual((b / d).to_casadi(), casadi.MX(1 / 2)) def test_convert_array_symbols(self): # Arrays a = np.array([1, 2, 3, 4, 5]) pybamm_a = pybamm.Array(a) - self.assertTrue(casadi.is_equal(pybamm_a.to_casadi(), casadi.SX(a))) + self.assert_casadi_equal(pybamm_a.to_casadi(), casadi.MX(a)) - casadi_t = casadi.SX.sym("t") - casadi_y = casadi.SX.sym("y", 10) + casadi_t = casadi.MX.sym("t") + casadi_y = casadi.MX.sym("y", 10) pybamm_t = pybamm.Time() pybamm_y = pybamm.StateVector(slice(0, 10)) @@ -64,22 +70,26 @@ def test_convert_array_symbols(self): self.assertEqual(pybamm_t.to_casadi(casadi_t, casadi_y), casadi_t) # State Vector - self.assertTrue( - casadi.is_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) - ) + self.assert_casadi_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) # outer product outer = pybamm.Outer(pybamm_a, pybamm_a) - self.assertTrue(casadi.is_equal(outer.to_casadi(), casadi.SX(outer.evaluate()))) + self.assert_casadi_equal( + outer.to_casadi(), casadi.MX(outer.evaluate()), evalf=True + ) def test_special_functions(self): a = pybamm.Array(np.array([1, 2, 3, 4, 5])) - self.assertEqual(pybamm.max(a).to_casadi(), casadi.SX(5)) - self.assertEqual(pybamm.min(a).to_casadi(), casadi.SX(1)) + self.assert_casadi_equal(pybamm.max(a).to_casadi(), casadi.MX(5), evalf=True) + self.assert_casadi_equal(pybamm.min(a).to_casadi(), casadi.MX(1), evalf=True) b = pybamm.Array(np.array([-2])) c = pybamm.Array(np.array([3])) - self.assertEqual(pybamm.Function(np.abs, b).to_casadi(), casadi.SX(2)) - self.assertEqual(pybamm.Function(np.abs, c).to_casadi(), casadi.SX(3)) + self.assert_casadi_equal( + pybamm.Function(np.abs, b).to_casadi(), casadi.MX(2), evalf=True + ) + self.assert_casadi_equal( + pybamm.Function(np.abs, c).to_casadi(), casadi.MX(3), evalf=True + ) def test_interpolation(self): x = np.linspace(0, 1)[:, np.newaxis] @@ -108,7 +118,9 @@ def test_concatenations(self): b = pybamm.Scalar(16) c = pybamm.Scalar(3) conc = pybamm.NumpyConcatenation(a, b, c) - self.assertTrue(casadi.is_equal(conc.to_casadi(), casadi.SX(conc.evaluate()))) + self.assert_casadi_equal( + conc.to_casadi(), casadi.MX(conc.evaluate()), evalf=True + ) # Domain concatenation mesh = get_mesh_for_testing() @@ -117,7 +129,9 @@ def test_concatenations(self): a = 2 * pybamm.Vector(np.ones_like(mesh[a_dom[0]][0].nodes), domain=a_dom) b = pybamm.Vector(np.ones_like(mesh[b_dom[0]][0].nodes), domain=b_dom) conc = pybamm.DomainConcatenation([b, a], mesh) - self.assertTrue(casadi.is_equal(conc.to_casadi(), casadi.SX(conc.evaluate()))) + self.assert_casadi_equal( + conc.to_casadi(), casadi.MX(conc.evaluate()), evalf=True + ) # 2d disc = get_1p1d_discretisation_for_testing() @@ -130,7 +144,7 @@ def test_concatenations(self): x = expr.to_casadi(None, y) f = casadi.Function("f", [x], [x]) y_eval = np.linspace(0, 1, expr.size) - self.assertTrue(casadi.is_equal(f(y_eval), casadi.SX(expr.evaluate(y=y_eval)))) + self.assert_casadi_equal(f(y_eval), casadi.SX(expr.evaluate(y=y_eval))) def test_convert_differentiated_function(self): a = pybamm.Scalar(0) @@ -141,15 +155,15 @@ def sin(x): return anp.sin(x) f = pybamm.Function(sin, b).diff(b) - self.assertEqual(f.to_casadi(), casadi.SX(np.cos(1))) + self.assert_casadi_equal(f.to_casadi(), casadi.MX(np.cos(1)), evalf=True) def myfunction(x, y): return x + y ** 3 f = pybamm.Function(myfunction, a, b).diff(a) - self.assertEqual(f.to_casadi(), casadi.SX(1)) + self.assert_casadi_equal(f.to_casadi(), casadi.MX(1), evalf=True) f = pybamm.Function(myfunction, a, b).diff(b) - self.assertEqual(f.to_casadi(), casadi.SX(3)) + self.assert_casadi_equal(f.to_casadi(), casadi.MX(3), evalf=True) def test_errors(self): y = pybamm.StateVector(slice(0, 10)) diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 3e3c3b3190..4550c3caff 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -26,7 +26,9 @@ def test_well_posed(self): model = pybamm.lead_acid.Composite( {"dimensionality": 1, "current collector": "potential pair"} ) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + self.assertIsInstance( + model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) + ) model.check_well_posedness() model = pybamm.lead_acid.Composite( @@ -61,7 +63,9 @@ def test_well_posed_algebraic(self): options = {"surface form": "algebraic", "side reactions": ["oxygen"]} model = pybamm.lead_acid.Composite(options) model.check_well_posedness() - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + self.assertIsInstance( + model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) + ) class TestLeadAcidCompositeExtended(unittest.TestCase): From bc31cd9e146f7a2a937df490f0c665b10ea4b195 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Thu, 7 Nov 2019 11:42:38 -0500 Subject: [PATCH 118/122] #685 fix interpolation example --- .../lico2_Marquis2019/lico2_data_example.csv | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv index f2f1809c79..5f2f5fef15 100644 --- a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv +++ b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv @@ -1,50 +1,50 @@ -0.000000000000000000e+00 4.714135898019971016e+00 -2.040816326530612082e-02 4.708899441575220557e+00 -4.081632653061224164e-02 4.702448345762175741e+00 -6.122448979591836593e-02 4.694558534379876136e+00 -8.163265306122448328e-02 4.684994372928071193e+00 -1.020408163265306006e-01 4.673523893805322516e+00 -1.224489795918367319e-01 4.659941254449398329e+00 -1.428571428571428492e-01 4.644096031712390271e+00 -1.632653061224489666e-01 4.625926611260677390e+00 -1.836734693877550839e-01 4.605491824833229053e+00 -2.040816326530612013e-01 4.582992038370575116e+00 -2.244897959183673186e-01 4.558769704421606228e+00 -2.448979591836734637e-01 4.533281647154224103e+00 -2.653061224489795533e-01 4.507041620859735254e+00 -2.857142857142856984e-01 4.480540404981123714e+00 -3.061224489795917880e-01 4.454158468368703439e+00 -3.265306122448979331e-01 4.428089899175588151e+00 -3.469387755102040782e-01 4.402295604083254155e+00 -3.673469387755101678e-01 4.376502631465185367e+00 -3.877551020408163129e-01 4.350272100879827519e+00 -4.081632653061224025e-01 4.323179536958428493e+00 -4.285714285714285476e-01 4.295195829713853719e+00 -4.489795918367346372e-01 4.267407675466301065e+00 -4.693877551020407823e-01 4.243081968022011985e+00 -4.897959183673469274e-01 4.220583168834260768e+00 -5.102040816326530726e-01 4.177032236370062712e+00 -5.306122448979591066e-01 4.134943568540559333e+00 -5.510204081632652517e-01 4.075402582839823928e+00 -5.714285714285713969e-01 4.055407164381796825e+00 -5.918367346938775420e-01 4.036052896449991323e+00 -6.122448979591835760e-01 4.012970397550268409e+00 -6.326530612244897211e-01 3.990385577539371287e+00 -6.530612244897958663e-01 3.970744780585252709e+00 -6.734693877551020114e-01 3.954753574690877738e+00 -6.938775510204081565e-01 3.942237451863396025e+00 -7.142857142857141906e-01 3.932683425747200534e+00 -7.346938775510203357e-01 3.925509771581312979e+00 -7.551020408163264808e-01 3.920182838859009422e+00 -7.755102040816326259e-01 3.916256861206461881e+00 -7.959183673469386600e-01 3.913378070528176877e+00 -8.163265306122448051e-01 3.911274218446639583e+00 -8.367346938775509502e-01 3.909739285381772067e+00 -8.571428571428570953e-01 3.908613829807601192e+00 -8.775510204081632404e-01 3.907726324580658162e+00 -8.979591836734692745e-01 3.906474088522892796e+00 -9.183673469387754196e-01 3.900204875423951556e+00 -9.387755102040815647e-01 3.848912814816038974e+00 -9.591836734693877098e-01 3.445226042113884724e+00 -9.795918367346938549e-01 1.687177743081021308e+00 -1.000000000000000000e+00 6.378908986260003328e-03 +0.000000000000000000e+00, 4.714135898019971016e+00 +2.040816326530612082e-02, 4.708899441575220557e+00 +4.081632653061224164e-02, 4.702448345762175741e+00 +6.122448979591836593e-02, 4.694558534379876136e+00 +8.163265306122448328e-02, 4.684994372928071193e+00 +1.020408163265306006e-01, 4.673523893805322516e+00 +1.224489795918367319e-01, 4.659941254449398329e+00 +1.428571428571428492e-01, 4.644096031712390271e+00 +1.632653061224489666e-01, 4.625926611260677390e+00 +1.836734693877550839e-01, 4.605491824833229053e+00 +2.040816326530612013e-01, 4.582992038370575116e+00 +2.244897959183673186e-01, 4.558769704421606228e+00 +2.448979591836734637e-01, 4.533281647154224103e+00 +2.653061224489795533e-01, 4.507041620859735254e+00 +2.857142857142856984e-01, 4.480540404981123714e+00 +3.061224489795917880e-01, 4.454158468368703439e+00 +3.265306122448979331e-01, 4.428089899175588151e+00 +3.469387755102040782e-01, 4.402295604083254155e+00 +3.673469387755101678e-01, 4.376502631465185367e+00 +3.877551020408163129e-01, 4.350272100879827519e+00 +4.081632653061224025e-01, 4.323179536958428493e+00 +4.285714285714285476e-01, 4.295195829713853719e+00 +4.489795918367346372e-01, 4.267407675466301065e+00 +4.693877551020407823e-01, 4.243081968022011985e+00 +4.897959183673469274e-01, 4.220583168834260768e+00 +5.102040816326530726e-01, 4.177032236370062712e+00 +5.306122448979591066e-01, 4.134943568540559333e+00 +5.510204081632652517e-01, 4.075402582839823928e+00 +5.714285714285713969e-01, 4.055407164381796825e+00 +5.918367346938775420e-01, 4.036052896449991323e+00 +6.122448979591835760e-01, 4.012970397550268409e+00 +6.326530612244897211e-01, 3.990385577539371287e+00 +6.530612244897958663e-01, 3.970744780585252709e+00 +6.734693877551020114e-01, 3.954753574690877738e+00 +6.938775510204081565e-01, 3.942237451863396025e+00 +7.142857142857141906e-01, 3.932683425747200534e+00 +7.346938775510203357e-01, 3.925509771581312979e+00 +7.551020408163264808e-01, 3.920182838859009422e+00 +7.755102040816326259e-01, 3.916256861206461881e+00 +7.959183673469386600e-01, 3.913378070528176877e+00 +8.163265306122448051e-01, 3.911274218446639583e+00 +8.367346938775509502e-01, 3.909739285381772067e+00 +8.571428571428570953e-01, 3.908613829807601192e+00 +8.775510204081632404e-01, 3.907726324580658162e+00 +8.979591836734692745e-01, 3.906474088522892796e+00 +9.183673469387754196e-01, 3.900204875423951556e+00 +9.387755102040815647e-01, 3.848912814816038974e+00 +9.591836734693877098e-01, 3.445226042113884724e+00 +9.795918367346938549e-01, 1.687177743081021308e+00 +1.000000000000000000e+00, 6.378908986260003328e-03 From bbc9bce8d36af31c7c6dc1a1d9668afd0abe43d6 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Thu, 7 Nov 2019 12:53:58 -0500 Subject: [PATCH 119/122] #685 fix integration tests --- pybamm/models/base_model.py | 5 +++-- .../lead_acid/base_lead_acid_model.py | 16 +++++++++++++++- pybamm/solvers/dae_solver.py | 12 ++++++++---- .../test_lead_acid/test_composite.py | 2 +- .../test_lead_acid/test_full.py | 1 + tests/integration/test_solvers/test_idaklu.py | 2 +- tests/unit/test_models/test_base_model.py | 4 ++++ tests/unit/test_solvers/test_idaklu_solver.py | 2 +- tests/unit/test_solvers/test_scikits_solvers.py | 2 +- 9 files changed, 35 insertions(+), 11 deletions(-) diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index a65a171725..08b33033ed 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -485,8 +485,9 @@ def default_solver(self): "Return default solver based on whether model is ODE model or DAE model" if len(self.algebraic) == 0: return pybamm.ScipySolver() - elif pybamm.have_idaklu(): + elif pybamm.have_idaklu() and self.use_jacobian is True: + # KLU solver requires jacobian to be provided return pybamm.IDAKLUSolver() else: - return pybamm.CasadiSolver() + return pybamm.CasadiSolver(mode="safe") diff --git a/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py b/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py index b0a50dd623..a25c7db88c 100644 --- a/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py +++ b/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py @@ -34,8 +34,22 @@ def default_geometry(self): @property def default_var_pts(self): + # Choose points that give uniform grid for the standard parameter values var = pybamm.standard_spatial_vars - return {var.x_n: 30, var.x_s: 30, var.x_p: 30, var.y: 10, var.z: 10} + return {var.x_n: 25, var.x_s: 41, var.x_p: 34, var.y: 10, var.z: 10} + + @property + def default_solver(self): + """ + Return default solver based on whether model is ODE model or DAE model. + There are bugs with KLU on the lead-acid models. + """ + if len(self.algebraic) == 0: + return pybamm.ScipySolver() + elif pybamm.have_scikits_odes(): + return pybamm.ScikitsDaeSolver() + else: + return pybamm.CasadiSolver(mode="safe") def set_standard_output_variables(self): super().set_standard_output_variables() diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index 1ee69adfe7..6032f8247a 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -282,10 +282,11 @@ def jacobian_alg(t, y): def rhs(t, y): return concatenated_rhs_fn(t, y).full()[:, 0] - def algebraic(t, y): - return concatenated_algebraic_fn(t, y).full()[:, 0] - if len(model.algebraic) > 0: + + def algebraic(t, y): + return concatenated_algebraic_fn(t, y).full()[:, 0] + y0 = self.calculate_consistent_initial_conditions( rhs, algebraic, @@ -293,7 +294,10 @@ def algebraic(t, y): jacobian_alg, ) else: - # can use DAE solver to solve ODE model + # can use DAE solver to solve ODE model (just return empty algebraic) + def algebraic(t, y): + return np.empty(0) + y0 = model.concatenated_initial_conditions[:, 0] # Create functions to evaluate residuals diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 1b81910416..05c2d9bb16 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -61,7 +61,7 @@ def test_basic_processing_algebraic(self): param = model.default_parameter_values param.update({"Typical current [A]": 1}) modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() + modeltest.test_all() # solver=pybamm.CasadiSolver()) class TestLeadAcidCompositeExtended(unittest.TestCase): diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py index 6d05b7cc74..0fe2b5f688 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -86,6 +86,7 @@ def test_set_up(self): if __name__ == "__main__": print("Add -v for more debug output") + # pybamm.set_logging_level("DEBUG") import sys if "-v" in sys.argv: diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index ac00023692..4ba46238f2 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -4,7 +4,7 @@ import unittest -@unittest.skipIf(~pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_on_spme(self): model = pybamm.lithium_ion.SPMe() diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 3978c06844..6421e41bce 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -381,6 +381,10 @@ def test_default_solver(self): model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) ) + # Check that turning off jacobian gives casadi solver + model.use_jacobian = False + self.assertIsInstance(model.default_solver, pybamm.CasadiSolver) + def test_default_parameters(self): # check parameters are read in ok model = pybamm.BaseBatteryModel() diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 4cf4f5af02..ac01bf66ca 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -7,7 +7,7 @@ import unittest -@unittest.skipIf(~pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index f9c4e5395e..af36063ff2 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -9,7 +9,7 @@ from tests import get_mesh_for_testing, get_discretisation_for_testing -@unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") +@unittest.skipIf(not pybamm.have_scikits_odes(), "scikits.odes not installed") class TestScikitsSolvers(unittest.TestCase): def test_ode_integrate(self): # Constant From 0029b95bc69fd0a8e76184e7ded4736d01d05dbf Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 7 Nov 2019 14:34:44 -0500 Subject: [PATCH 120/122] #715 update sundials to 4.1.0 --- INSTALL-LINUX.md | 16 ++++++++-------- scripts/install_scikits_odes.sh | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/INSTALL-LINUX.md b/INSTALL-LINUX.md index fb0766a89b..34ef1f2a57 100644 --- a/INSTALL-LINUX.md +++ b/INSTALL-LINUX.md @@ -94,7 +94,7 @@ Before installing scikits.odes, you need to have installed: - Fortran compiler (e.g. gfortran) - BLAS/LAPACK install (OpenBLAS is recommended by the scikits.odes developers) - CMake (for building Sundials) -- Sundials 3.1.1 +- Sundials 4.1.0 You can install these on Ubuntu or Debian using apt-get: @@ -102,17 +102,17 @@ You can install these on Ubuntu or Debian using apt-get: sudo apt-get install python3-dev gfortran gcc cmake libopenblas-dev ``` -To install Sundials 3.1.1, on the command-line type: +To install Sundials 4.1.0, on the command-line type: ```bash INSTALL_DIR=`pwd`/sundials -wget https://computation.llnl.gov/projects/sundials/download/sundials-3.1.1.tar.gz -tar -xvf sundials-3.1.1.tar.gz -mkdir build-sundials-3.1.1 -cd build-sundials-3.1.1/ -cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-3.1.1/ +wget https://computation.llnl.gov/projects/sundials/download/sundials-4.1.0.tar.gz +tar -xvf sundials-4.1.0.tar.gz +mkdir build-sundials-4.1.0 +cd build-sundials-4.1.0/ +cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-4.1.0/ make install -rm -r ../sundials-3.1.1 +rm -r ../sundials-4.1.0 ``` Then install [scikits.odes](https://github.com/bmcage/odes), letting it know the sundials install location: diff --git a/scripts/install_scikits_odes.sh b/scripts/install_scikits_odes.sh index 8d91aef507..f88135b413 100755 --- a/scripts/install_scikits_odes.sh +++ b/scripts/install_scikits_odes.sh @@ -1,7 +1,7 @@ #!/bin/bash -SUNDIALS_URL=https://github.com/LLNL/sundials/archive/v3.1.1.tar.gz -SUNDIALS_NAME=sundials-3.1.1.tar.gz +SUNDIALS_URL=https://github.com/LLNL/sundials/archive/v4.1.0.tar.gz +SUNDIALS_NAME=sundials-4.1.0.tar.gz CURRENT_DIR=`pwd` TMP_DIR=$CURRENT_DIR/tmp mkdir $TMP_DIR @@ -10,9 +10,9 @@ INSTALL_DIR=$CURRENT_DIR/sundials cd $TMP_DIR wget $SUNDIALS_URL -O $SUNDIALS_NAME tar -xvf $SUNDIALS_NAME -mkdir build-sundials-3.1.1 -cd build-sundials-3.1.1/ -cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-3.1.1/ +mkdir build-sundials-4.1.0 +cd build-sundials-4.1.0/ +cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-4.1.0/ make install cd $CURRENT_DIR rm -rf $TMP_DIR From 6ffcf8182fda9111253a3a0b83fdb1624888b258 Mon Sep 17 00:00:00 2001 From: tinosulzer Date: Thu, 7 Nov 2019 15:12:44 -0500 Subject: [PATCH 121/122] #685 update changelog --- CHANGELOG.md | 29 ++++++++++++++------------- pybamm/expression_tree/interpolant.py | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f1f641b4..066d81d63e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,30 +3,31 @@ ## Features - Added Simulation class ([#693](https://github.com/pybamm-team/PyBaMM/pull/693)) -- Add interface to CasADi solver ([#687](https://github.com/pybamm-team/PyBaMM/pull/687), [#691](https://github.com/pybamm-team/PyBaMM/pull/691)) -- Add option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) -- Add method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) -- Add `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) -- Add `Interpolant` class to interpolate experimental data (e.g. OCP curves) ([#661](https://github.com/pybamm-team/PyBaMM/pull/661)) +- Added interface to CasADi solver ([#687](https://github.com/pybamm-team/PyBaMM/pull/687), [#691](https://github.com/pybamm-team/PyBaMM/pull/691), [#714](https://github.com/pybamm-team/PyBaMM/pull/714)). This makes the SUNDIALS DAE solvers (Scikits and KLU) truly optional (though IDA KLU is recommended for solving the DFN). +- Added option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) +- Added method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) +- Added `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) +- Added `Interpolant` class to interpolate experimental data (e.g. OCP curves) ([#661](https://github.com/pybamm-team/PyBaMM/pull/661)) - Added interface (via pybind11) to sundials with the IDA KLU sparse linear solver ([#657](https://github.com/pybamm-team/PyBaMM/pull/657)) -- Allow parameters to be set by material or by specifying a particular paper ([#647](https://github.com/pybamm-team/PyBaMM/pull/647)) +- Allowed parameters to be set by material or by specifying a particular paper ([#647](https://github.com/pybamm-team/PyBaMM/pull/647)) - Set relative and absolute tolerances independently in solvers ([#645](https://github.com/pybamm-team/PyBaMM/pull/645)) -- Add basic method to allow (a part of) the State Vector to be updated with results obtained from another solution or package ([#624](https://github.com/pybamm-team/PyBaMM/pull/624)) -- Add some non-uniform meshes in 1D and 2D ([#617](https://github.com/pybamm-team/PyBaMM/pull/617)) +- Added basic method to allow (a part of) the State Vector to be updated with results obtained from another solution or package ([#624](https://github.com/pybamm-team/PyBaMM/pull/624)) +- Added some non-uniform meshes in 1D and 2D ([#617](https://github.com/pybamm-team/PyBaMM/pull/617)) ## Optimizations +- Use CasADi's automatic differentation algorithms by default when solving a model ([#714](https://github.com/pybamm-team/PyBaMM/pull/714)) - Avoid re-checking size when making a copy of an `Index` object ([#656](https://github.com/pybamm-team/PyBaMM/pull/656)) - Avoid recalculating `_evaluation_array` when making a copy of a `StateVector` object ([#653](https://github.com/pybamm-team/PyBaMM/pull/653)) ## Bug fixes -- Correct a sign error in Dirichlet boundary conditions in the Finite Element Method ([#706](https://github.com/pybamm-team/PyBaMM/pull/706)) -- Pass the correct dimensional temperature to open circuit potential ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) -- Adds missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) -- Fix differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) -- Add warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) -- Improve the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) +- Corrected a sign error in Dirichlet boundary conditions in the Finite Element Method ([#706](https://github.com/pybamm-team/PyBaMM/pull/706)) +- Passed the correct dimensional temperature to open circuit potential ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) +- Added missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) +- Fixed differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) +- Added warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) +- Improved the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) # [v0.1.0](https://github.com/pybamm-team/PyBaMM/tree/v0.1.0) - 2019-10-08 diff --git a/pybamm/expression_tree/interpolant.py b/pybamm/expression_tree/interpolant.py index d60f7b23c1..d3e3758602 100644 --- a/pybamm/expression_tree/interpolant.py +++ b/pybamm/expression_tree/interpolant.py @@ -73,7 +73,7 @@ def _function_new_copy(self, children): *children, name=self.name, interpolator=self.interpolator, - extrapolate=self.extrapolate, + extrapolate=self.extrapolate ) def _function_simplify(self, simplified_children): From 8829213a6408a2f64668075065fca6db4b80f26a Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 7 Nov 2019 16:02:26 -0500 Subject: [PATCH 122/122] #685 update solver tests --- .../test_full_battery_models/test_lead_acid/test_composite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 4550c3caff..183fe3cc46 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -27,7 +27,7 @@ def test_well_posed(self): {"dimensionality": 1, "current collector": "potential pair"} ) self.assertIsInstance( - model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) + model.default_solver, (pybamm.ScikitsDaeSolver, pybamm.CasadiSolver) ) model.check_well_posedness() @@ -64,7 +64,7 @@ def test_well_posed_algebraic(self): model = pybamm.lead_acid.Composite(options) model.check_well_posedness() self.assertIsInstance( - model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) + model.default_solver, (pybamm.ScikitsDaeSolver, pybamm.CasadiSolver) )