From 0d94d254bd036eed3cf4ff4e32245ba186279654 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 17 Jun 2020 12:19:59 +0100 Subject: [PATCH 01/67] added BasicMPM class to lithium_ion --- .../lithium_ion/__init__.py | 1 + .../lithium_ion/basic_mpm.py | 381 ++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 pybamm/models/full_battery_models/lithium_ion/basic_mpm.py diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/pybamm/models/full_battery_models/lithium_ion/__init__.py index 167a38f3b5..f23da30579 100644 --- a/pybamm/models/full_battery_models/lithium_ion/__init__.py +++ b/pybamm/models/full_battery_models/lithium_ion/__init__.py @@ -7,3 +7,4 @@ from .dfn import DFN from .basic_dfn import BasicDFN from .basic_spm import BasicSPM +from .basic_mpm import BasicMPM diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py new file mode 100644 index 0000000000..95ff69d597 --- /dev/null +++ b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py @@ -0,0 +1,381 @@ +# +# Basic Many Particle Model (MPM) +# +import pybamm +from .base_lithium_ion_model import BaseModel + + +class BasicMPM(BaseModel): + """Many Particle Model (MPM) model of a lithium-ion battery, from [1]_. + + This class differs from the :class:`pybamm.lithium_ion.SPM` model class in that it + shows the whole model in a single class. This comes at the cost of flexibility in + combining different physical effects, and in general the main SPM class should be + used instead. + + Parameters + ---------- + name : str, optional + The name of the model. + + References + ---------- + .. [1] TL Kirk, J Evans, CP Please and SJ Chapman. “Modelling electrode heterogeneity + in lithium-ion batteries: unimodal and bimodal particle-size distributions”. + In: arXiv preprint arXiv:????? (2020). + + + **Extends:** :class:`pybamm.lithium_ion.BaseModel` + """ + + def __init__(self, name="Many Particle Model"): + super().__init__({}, name) + ###################### + # Parameters + ###################### + # Import all the standard parameters from base_lithium_ion_model.BaseModel + # (in turn from pybamm.standard_parameters_lithium_ion) + param = self.param + + # additional parameters + sd_a_n = pybamm.Parameter("negative area-weighted particle size standard deviation") + sd_a_p = pybamm.Parameter("positive area-weighted particle size standard deviation") + + def f_a_dist_n(R,R_av_a,sd_a): + return pybamm.FunctionParameter( + "negative area-weighted particle size distribution", + { + "negative particle size variable": R, + "negative area-weighted mean particle size": R_av_a, + "negative area-weighted particle size standard deviation": sd_a, + } + ) + def f_a_dist_p(R,R_av_a,sd_a): + return pybamm.FunctionParameter( + "positive area-weighted particle size distribution", + { + "positive particle size variable": R, + "positive area-weighted mean particle size": R_av_a, + "positive area-weighted particle size standard deviation": sd_a, + } + ) + + + ###################### + # Variables + ###################### + # Discharge capacity + Q = pybamm.Variable("Discharge capacity [A.h]") + # X-averaged particle concentrations + c_s_n = pybamm.Variable( + "X-averaged negative particle concentration", + domain="negative particle", + auxiliary_domains={ + "secondary": "negative particle size domain", + #"tertiary": "negative electrode", + } + ) + c_s_p = pybamm.Variable( + "X-averaged positive particle concentration", + domain="positive particle", + auxiliary_domains={ + "secondary": "positive particle size domain", + #"tertiary": "positive electrode" + } + ) + # Electrode potentials (leave them with a domain for now) + phi_s_n = pybamm.Variable( + "Negative electrode potential" +# domain="negative particle size domain", +# auxiliary_domains={ +# "secondary": "negative electrode", +# } + ) + phi_s_p = pybamm.Variable( + "Positive electrode potential" +# domain="positive particle size domain", +# auxiliary_domains={ +# "secondary": "positive electrode", +# } + ) + + # Spatial Variables + R_variable_n = pybamm.SpatialVariable( + "negative particle size variable", + domain=["negative particle size domain"], + coord_sys="cartesian" + ) + R_variable_p = pybamm.SpatialVariable( + "positive particle size variable", + domain=["positive particle size domain"], + coord_sys="cartesian" + ) + + # Constant temperature + T = param.T_init + + ###################### + # Other set-up + ###################### + + # Current density + i_cell = param.current_with_time +# j_n = i_cell / param.l_n +# j_p = -i_cell / param.l_p + + ###################### + # State of Charge + ###################### + I = param.dimensional_current_with_time + # The `rhs` dictionary contains differential equations, with the key being the + # variable in the d/dt + self.rhs[Q] = I * param.timescale / 3600 + # Initial conditions must be provided for the ODEs + self.initial_conditions[Q] = pybamm.Scalar(0) + + ###################### + # Interfacial reactions + ###################### + + c_s_surf_n = pybamm.surf(c_s_n) + phi_e = 0 + + + j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n + j_n = ( + 2 + * j0_n + * pybamm.sinh( + param.ne_n / 2 * (phi_s_n + - phi_e - param.U_n(c_s_surf_n, T)) + ) + ) + c_s_surf_p = pybamm.surf(c_s_p) + j0_p = param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p + + j_p = ( + 2 + * j0_p + * pybamm.sinh( + param.ne_p / 2 * (phi_s_p - phi_e - param.U_p(c_s_surf_p, T)) + ) + ) + + self.algebraic[phi_s_n] = pybamm.Integral( + f_a_dist_n(R_variable_n, 1, sd_a_n)*j_n, + R_variable_n + ) - i_cell/param.l_n + + self.algebraic[phi_s_p] = pybamm.Integral( + f_a_dist_p(R_variable_p, 1, sd_a_p)*j_p, + R_variable_p + ) + i_cell/param.l_p + + self.initial_conditions[phi_s_n] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "negative particle size domain") + + self.initial_conditions[phi_s_p] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "positive particle size domain") + + ###################### + # Particles + ###################### + + # The div and grad operators will be converted to the appropriate matrix + # multiplication at the discretisation stage + N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p) + self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n) / R_variable_n**2 + self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p) / R_variable_p**2 + + # Boundary conditions must be provided for equations with spatial derivatives + self.boundary_conditions[c_s_n] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": ( + -R_variable_n * param.C_n * j_n / param.a_n / param.D_n(c_s_surf_n, T), + "Neumann", + ), + } + self.boundary_conditions[c_s_p] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": ( + -R_variable_p * param.C_p * j_p / param.a_p / param.gamma_p / param.D_p(c_s_surf_p, T), + "Neumann", + ), + } + # c_n_init and c_p_init are functions of x, but for the SPM we evaluate them at x=0 + # and x=1 since there is no x-dependence in the particles + self.initial_conditions[c_s_n] = param.c_n_init(0) + self.initial_conditions[c_s_p] = param.c_p_init(1) + + # Events specify points at which a solution should terminate + self.events += [ + pybamm.Event( + "Minimum negative particle surface concentration", + pybamm.min(c_s_surf_n) - 0.01, + ), + pybamm.Event( + "Maximum negative particle surface concentration", + (1 - 0.01) - pybamm.max(c_s_surf_n), + ), + pybamm.Event( + "Minimum positive particle surface concentration", + pybamm.min(c_s_surf_p) - 0.01, + ), + pybamm.Event( + "Maximum positive particle surface concentration", + (1 - 0.01) - pybamm.max(c_s_surf_p), + ), + ] + + + # The `variables` dictionary contains all variables that might be useful for + # visualising the solution of the model + # Primary broadcasts are used to broadcast scalar quantities across a domain + # into a vector of the right shape, for multiplying with other vectors + + V = phi_s_p - phi_s_n + whole_cell = ["negative electrode", "separator", "positive electrode"] + + self.variables = { + "Negative particle surface concentration": c_s_surf_n, + "Electrolyte concentration": pybamm.PrimaryBroadcast(1, whole_cell), + "Positive particle surface concentration": c_s_surf_p, + "Current [A]": I, + "Negative electrode potential": pybamm.PrimaryBroadcast( + phi_s_n, "negative electrode" + ), + "Electrolyte potential": pybamm.PrimaryBroadcast(phi_e, whole_cell), + "Positive electrode potential": pybamm.PrimaryBroadcast( + phi_s_p, "positive electrode" + ), + "Terminal voltage": V, + } + self.events += [ + pybamm.Event("Minimum voltage", V - param.voltage_low_cut), + pybamm.Event("Maximum voltage", V - param.voltage_high_cut), + ] + + + @property + def default_parameter_values(self): + # Default parameter values + # Lion parameters left as default parameter set for tests + import numpy as np + + def f_a_dist_Gaussian(R,R_av_a,sd_a): + return pybamm.exp(-(R-R_av_a)**2/(2*sd_a**2))/pybamm.sqrt(2*np.pi)/sd_a + + default_params = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019) + default_params.update({"negative area-weighted particle size standard deviation": 0.3}, check_already_exists=False) + default_params.update({"positive area-weighted particle size standard deviation": 0.3}, check_already_exists=False) + default_params.update({"negative area-weighted particle size distribution": f_a_dist_Gaussian}, check_already_exists=False) + default_params.update({"positive area-weighted particle size distribution": f_a_dist_Gaussian}, check_already_exists=False) + + return default_params + + @property + def default_geometry(self): + default_geom = pybamm.battery_geometry() + + # New Spatial Variables + R_variable_n = pybamm.SpatialVariable( + "negative particle size variable", + domain=["negative particle size domain"], + coord_sys="cartesian" + ) + R_variable_p = pybamm.SpatialVariable( + "positive particle size variable", + domain=["positive particle size domain"], + coord_sys="cartesian" + ) + + # input new domains + default_geom.update({"negative particle size domain": + { + R_variable_n: { + "min": pybamm.Scalar(0), + "max": pybamm.Scalar(5), + } + } + } + ) + default_geom.update({"positive particle size domain": + { + R_variable_p: { + "min": pybamm.Scalar(0), + "max": pybamm.Scalar(5), + } + } + } + ) + return default_geom + + @property + def default_var_pts(self): + var = pybamm.standard_spatial_vars + + # New Spatial Variables + R_variable_n = pybamm.SpatialVariable( + "negative particle size variable", + domain=["negative particle size domain"], + coord_sys="cartesian" + ) + R_variable_p = pybamm.SpatialVariable( + "positive particle size variable", + domain=["positive particle size domain"], + coord_sys="cartesian" + ) + + return { + var.x_n: 20, + var.x_s: 20, + var.x_p: 20, + var.r_n: 10, + var.r_p: 10, + var.y: 10, + var.z: 10, + R_variable_n: 30, + R_variable_p: 30, + } + + @property + def default_submesh_types(self): + base_submeshes = { + "negative electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "separator": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "negative particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "positive particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "negative particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "positive particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + } + if self.options["dimensionality"] == 0: + base_submeshes["current collector"] = pybamm.MeshGenerator(pybamm.SubMesh0D) + elif self.options["dimensionality"] == 1: + base_submeshes["current collector"] = pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ) + elif self.options["dimensionality"] == 2: + base_submeshes["current collector"] = pybamm.MeshGenerator( + pybamm.ScikitUniform2DSubMesh + ) + return base_submeshes + + @property + def default_spatial_methods(self): + base_spatial_methods = { + "macroscale": pybamm.FiniteVolume(), + "negative particle": pybamm.FiniteVolume(), + "positive particle": pybamm.FiniteVolume(), + "negative particle size domain": pybamm.FiniteVolume(), + "positive particle size domain": pybamm.FiniteVolume(), + } + if self.options["dimensionality"] == 0: + # 0D submesh - use base spatial method + base_spatial_methods[ + "current collector" + ] = pybamm.ZeroDimensionalSpatialMethod() + elif self.options["dimensionality"] == 1: + base_spatial_methods["current collector"] = pybamm.FiniteVolume() + elif self.options["dimensionality"] == 2: + base_spatial_methods["current collector"] = pybamm.ScikitFiniteElement() + return base_spatial_methods From 095484cb4c49e251b14826123c9618f0cd191f51 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 18 Jun 2020 18:32:28 +0100 Subject: [PATCH 02/67] expanded output variables --- .../lithium_ion/basic_mpm.py | 242 ++++++++++-------- 1 file changed, 132 insertions(+), 110 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py index 95ff69d597..0092789556 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py @@ -8,10 +8,8 @@ class BasicMPM(BaseModel): """Many Particle Model (MPM) model of a lithium-ion battery, from [1]_. - This class differs from the :class:`pybamm.lithium_ion.SPM` model class in that it - shows the whole model in a single class. This comes at the cost of flexibility in - combining different physical effects, and in general the main SPM class should be - used instead. + This class is similar to the :class:`pybamm.lithium_ion.SPM` model class in that it + shows the whole model in a single class. Parameters ---------- @@ -37,27 +35,31 @@ def __init__(self, name="Many Particle Model"): # (in turn from pybamm.standard_parameters_lithium_ion) param = self.param - # additional parameters + # Additional parameters for this model + # Dimensionless standard deviations sd_a_n = pybamm.Parameter("negative area-weighted particle size standard deviation") sd_a_p = pybamm.Parameter("positive area-weighted particle size standard deviation") + # Particle size distributions (area-weighted) def f_a_dist_n(R,R_av_a,sd_a): - return pybamm.FunctionParameter( - "negative area-weighted particle size distribution", - { + inputs = { "negative particle size variable": R, "negative area-weighted mean particle size": R_av_a, "negative area-weighted particle size standard deviation": sd_a, } + return pybamm.FunctionParameter( + "negative area-weighted particle size distribution", + inputs, ) def f_a_dist_p(R,R_av_a,sd_a): - return pybamm.FunctionParameter( - "positive area-weighted particle size distribution", - { + inputs = { "positive particle size variable": R, "positive area-weighted mean particle size": R_av_a, "positive area-weighted particle size standard deviation": sd_a, } + return pybamm.FunctionParameter( + "positive area-weighted particle size distribution", + inputs, ) @@ -66,7 +68,8 @@ def f_a_dist_p(R,R_av_a,sd_a): ###################### # Discharge capacity Q = pybamm.Variable("Discharge capacity [A.h]") - # X-averaged particle concentrations + # X-averaged particle concentrations: these now depend continuously on particle size, and so + # have secondary domains "negative/positive particle size domain" c_s_n = pybamm.Variable( "X-averaged negative particle concentration", domain="negative particle", @@ -83,13 +86,9 @@ def f_a_dist_p(R,R_av_a,sd_a): #"tertiary": "positive electrode" } ) - # Electrode potentials (leave them with a domain for now) - phi_s_n = pybamm.Variable( - "Negative electrode potential" -# domain="negative particle size domain", -# auxiliary_domains={ -# "secondary": "negative electrode", -# } + # Electrode potentials (leave them without a domain for now) + phi_e = pybamm.Variable( + "Electrolyte potential" ) phi_s_p = pybamm.Variable( "Positive electrode potential" @@ -102,12 +101,12 @@ def f_a_dist_p(R,R_av_a,sd_a): # Spatial Variables R_variable_n = pybamm.SpatialVariable( "negative particle size variable", - domain=["negative particle size domain"], + domain=["negative particle size domain"], #could add auxiliary domains coord_sys="cartesian" ) R_variable_p = pybamm.SpatialVariable( "positive particle size variable", - domain=["positive particle size domain"], + domain=["positive particle size domain"], #could add auxiliary domains coord_sys="cartesian" ) @@ -120,8 +119,6 @@ def f_a_dist_p(R,R_av_a,sd_a): # Current density i_cell = param.current_with_time -# j_n = i_cell / param.l_n -# j_p = -i_cell / param.l_p ###################### # State of Charge @@ -138,20 +135,19 @@ def f_a_dist_p(R,R_av_a,sd_a): ###################### c_s_surf_n = pybamm.surf(c_s_n) - phi_e = 0 + phi_s_n = 0 - j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n + j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n # setting c_e = 1 j_n = ( 2 * j0_n * pybamm.sinh( - param.ne_n / 2 * (phi_s_n - - phi_e - param.U_n(c_s_surf_n, T)) + param.ne_n / 2 * (phi_s_n - phi_e - param.U_n(c_s_surf_n, T)) ) ) c_s_surf_p = pybamm.surf(c_s_p) - j0_p = param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p + j0_p = param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p # setting c_e = 1 j_p = ( 2 @@ -161,18 +157,19 @@ def f_a_dist_p(R,R_av_a,sd_a): ) ) - self.algebraic[phi_s_n] = pybamm.Integral( + # integral equation for phi_e + self.algebraic[phi_e] = pybamm.Integral( f_a_dist_n(R_variable_n, 1, sd_a_n)*j_n, R_variable_n ) - i_cell/param.l_n + # integral equation for phi_s_p self.algebraic[phi_s_p] = pybamm.Integral( f_a_dist_p(R_variable_p, 1, sd_a_p)*j_p, R_variable_p ) + i_cell/param.l_p - self.initial_conditions[phi_s_n] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "negative particle size domain") - + self.initial_conditions[phi_e] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "negative particle size domain") self.initial_conditions[phi_s_p] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "positive particle size domain") ###################### @@ -232,49 +229,99 @@ def f_a_dist_p(R,R_av_a,sd_a): # Primary broadcasts are used to broadcast scalar quantities across a domain # into a vector of the right shape, for multiplying with other vectors - V = phi_s_p - phi_s_n + V = phi_s_p + + # Dimensional output variables + V_dim = param.potential_scale * V + (param.U_p_ref - param.U_n_ref) + c_s_surf_n_dim = c_s_surf_n + c_s_surf_p_dim = c_s_surf_p + c_e = 1 + c_e_dim = c_e + phi_s_n_dim = phi_s_n + phi_s_p_dim = phi_s_p + phi_e_dim = phi_e + + + + + whole_cell = ["negative electrode", "separator", "positive electrode"] self.variables = { "Negative particle surface concentration": c_s_surf_n, - "Electrolyte concentration": pybamm.PrimaryBroadcast(1, whole_cell), + "Negative particle surface concentration [mol.m-3]": c_s_surf_n_dim, + "Electrolyte concentration": pybamm.PrimaryBroadcast(c_e, whole_cell), + "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast(c_e_dim, whole_cell), "Positive particle surface concentration": c_s_surf_p, + "Positive particle surface concentration [mol.m-3]": c_s_surf_p_dim, "Current [A]": I, "Negative electrode potential": pybamm.PrimaryBroadcast( phi_s_n, "negative electrode" ), + "Negative electrode potential [V]": pybamm.PrimaryBroadcast( + phi_s_n_dim, "negative electrode" + ), "Electrolyte potential": pybamm.PrimaryBroadcast(phi_e, whole_cell), + "Electrolyte potential [V]": pybamm.PrimaryBroadcast(phi_e_dim, whole_cell), "Positive electrode potential": pybamm.PrimaryBroadcast( phi_s_p, "positive electrode" ), + "Positive electrode potential [V]": pybamm.PrimaryBroadcast( + phi_s_p_dim, "positive electrode" + ), "Terminal voltage": V, + "Terminal voltage [V]": V_dim, } + + self.events += [ pybamm.Event("Minimum voltage", V - param.voltage_low_cut), pybamm.Event("Maximum voltage", V - param.voltage_high_cut), ] + #################### + # Overwrite defaults + #################### @property def default_parameter_values(self): # Default parameter values # Lion parameters left as default parameter set for tests - import numpy as np + default_params = super().default_parameter_values + - def f_a_dist_Gaussian(R,R_av_a,sd_a): - return pybamm.exp(-(R-R_av_a)**2/(2*sd_a**2))/pybamm.sqrt(2*np.pi)/sd_a + # append new parameter values - default_params = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019) - default_params.update({"negative area-weighted particle size standard deviation": 0.3}, check_already_exists=False) - default_params.update({"positive area-weighted particle size standard deviation": 0.3}, check_already_exists=False) - default_params.update({"negative area-weighted particle size distribution": f_a_dist_Gaussian}, check_already_exists=False) - default_params.update({"positive area-weighted particle size distribution": f_a_dist_Gaussian}, check_already_exists=False) + # lognormal area-weighted particle size distribution + def lognormal_dist(R,R_av,sd): + import numpy as np + # inputs are particle radius R, the mean R_av, and standard deviation sd + mu_ln = pybamm.log(R_av**2/pybamm.sqrt(R_av**2+sd**2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd**2/R_av**2)) + return pybamm.exp(-(pybamm.log(R)-mu_ln)**2/(2*sigma_ln**2))/pybamm.sqrt(2*np.pi*sigma_ln**2)/R + + default_params.update( + {"negative area-weighted particle size standard deviation": 0.3}, + check_already_exists=False + ) + default_params.update( + {"positive area-weighted particle size standard deviation": 0.3}, + check_already_exists=False + ) + default_params.update( + {"negative area-weighted particle size distribution": lognormal_dist}, + check_already_exists=False + ) + default_params.update( + {"positive area-weighted particle size distribution": lognormal_dist}, + check_already_exists=False + ) return default_params @property def default_geometry(self): - default_geom = pybamm.battery_geometry() + default_geom = super().default_geometry # New Spatial Variables R_variable_n = pybamm.SpatialVariable( @@ -288,94 +335,69 @@ def default_geometry(self): coord_sys="cartesian" ) - # input new domains - default_geom.update({"negative particle size domain": + # append new domains + default_geom.update( { - R_variable_n: { - "min": pybamm.Scalar(0), - "max": pybamm.Scalar(5), - } + "negative particle size domain": { + R_variable_n: { + "min": pybamm.Scalar(0), + "max": pybamm.Scalar(5), + } + }, + "positive particle size domain": { + R_variable_p: { + "min": pybamm.Scalar(0), + "max": pybamm.Scalar(5), + } + }, } - } - ) - default_geom.update({"positive particle size domain": - { - R_variable_p: { - "min": pybamm.Scalar(0), - "max": pybamm.Scalar(5), - } - } - } ) return default_geom @property def default_var_pts(self): - var = pybamm.standard_spatial_vars + defaults = super().default_var_pts # New Spatial Variables R_variable_n = pybamm.SpatialVariable( "negative particle size variable", domain=["negative particle size domain"], coord_sys="cartesian" - ) + ) R_variable_p = pybamm.SpatialVariable( "positive particle size variable", domain=["positive particle size domain"], coord_sys="cartesian" - ) - - return { - var.x_n: 20, - var.x_s: 20, - var.x_p: 20, - var.r_n: 10, - var.r_p: 10, - var.y: 10, - var.z: 10, - R_variable_n: 30, - R_variable_p: 30, - } + ) + # add to dictionary + defaults.update( + { + R_variable_n: 50, + R_variable_p: 50, + } + ) + return defaults @property def default_submesh_types(self): - base_submeshes = { - "negative electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "separator": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "negative particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "positive particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "negative particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "positive particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - } - if self.options["dimensionality"] == 0: - base_submeshes["current collector"] = pybamm.MeshGenerator(pybamm.SubMesh0D) - elif self.options["dimensionality"] == 1: - base_submeshes["current collector"] = pybamm.MeshGenerator( - pybamm.Uniform1DSubMesh - ) - elif self.options["dimensionality"] == 2: - base_submeshes["current collector"] = pybamm.MeshGenerator( - pybamm.ScikitUniform2DSubMesh - ) - return base_submeshes + default_submeshes = super().default_submesh_types + + default_submeshes.update( + { + "negative particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "positive particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + } + ) + return default_submeshes @property def default_spatial_methods(self): - base_spatial_methods = { - "macroscale": pybamm.FiniteVolume(), - "negative particle": pybamm.FiniteVolume(), - "positive particle": pybamm.FiniteVolume(), - "negative particle size domain": pybamm.FiniteVolume(), - "positive particle size domain": pybamm.FiniteVolume(), - } - if self.options["dimensionality"] == 0: - # 0D submesh - use base spatial method - base_spatial_methods[ - "current collector" - ] = pybamm.ZeroDimensionalSpatialMethod() - elif self.options["dimensionality"] == 1: - base_spatial_methods["current collector"] = pybamm.FiniteVolume() - elif self.options["dimensionality"] == 2: - base_spatial_methods["current collector"] = pybamm.ScikitFiniteElement() - return base_spatial_methods + default_spatials = super().default_spatial_methods + + default_spatials.update( + { + "negative particle size domain": pybamm.FiniteVolume(), + "positive particle size domain": pybamm.FiniteVolume(), + } + ) + return default_spatials From ec61fb3be18e923137acf11003389b15a18a9835 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 24 Jun 2020 10:39:46 +0100 Subject: [PATCH 03/67] rename model class --- .../lithium_ion/__init__.py | 2 +- .../lithium_ion/basic_mpm.py | 173 ++++++++++++------ 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/pybamm/models/full_battery_models/lithium_ion/__init__.py index f23da30579..0eecff6942 100644 --- a/pybamm/models/full_battery_models/lithium_ion/__init__.py +++ b/pybamm/models/full_battery_models/lithium_ion/__init__.py @@ -7,4 +7,4 @@ from .dfn import DFN from .basic_dfn import BasicDFN from .basic_spm import BasicSPM -from .basic_mpm import BasicMPM +from .basic_mpm import BasicPSDModel diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py index 0092789556..19a90e2966 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py @@ -5,8 +5,8 @@ from .base_lithium_ion_model import BaseModel -class BasicMPM(BaseModel): - """Many Particle Model (MPM) model of a lithium-ion battery, from [1]_. +class BasicPSDModel(BaseModel): + """Particle-Size Distribution (PSD) model of a lithium-ion battery, from [1]_. This class is similar to the :class:`pybamm.lithium_ion.SPM` model class in that it shows the whole model in a single class. @@ -26,7 +26,7 @@ class BasicMPM(BaseModel): **Extends:** :class:`pybamm.lithium_ion.BaseModel` """ - def __init__(self, name="Many Particle Model"): + def __init__(self, name="Particle-Size Distribution Model"): super().__init__({}, name) ###################### # Parameters @@ -37,28 +37,28 @@ def __init__(self, name="Many Particle Model"): # Additional parameters for this model # Dimensionless standard deviations - sd_a_n = pybamm.Parameter("negative area-weighted particle size standard deviation") - sd_a_p = pybamm.Parameter("positive area-weighted particle size standard deviation") + sd_a_n = pybamm.Parameter("negative area-weighted particle-size standard deviation") + sd_a_p = pybamm.Parameter("positive area-weighted particle-size standard deviation") - # Particle size distributions (area-weighted) + # Particle-size distributions (area-weighted) def f_a_dist_n(R,R_av_a,sd_a): inputs = { - "negative particle size variable": R, + "negative particle-size variable": R, "negative area-weighted mean particle size": R_av_a, - "negative area-weighted particle size standard deviation": sd_a, + "negative area-weighted particle-size standard deviation": sd_a, } return pybamm.FunctionParameter( - "negative area-weighted particle size distribution", + "negative area-weighted particle-size distribution", inputs, ) def f_a_dist_p(R,R_av_a,sd_a): inputs = { - "positive particle size variable": R, + "positive particle-size variable": R, "positive area-weighted mean particle size": R_av_a, - "positive area-weighted particle size standard deviation": sd_a, + "positive area-weighted particle-size standard deviation": sd_a, } return pybamm.FunctionParameter( - "positive area-weighted particle size distribution", + "positive area-weighted particle-size distribution", inputs, ) @@ -69,12 +69,12 @@ def f_a_dist_p(R,R_av_a,sd_a): # Discharge capacity Q = pybamm.Variable("Discharge capacity [A.h]") # X-averaged particle concentrations: these now depend continuously on particle size, and so - # have secondary domains "negative/positive particle size domain" + # have secondary domains "negative/positive particle-size domain" c_s_n = pybamm.Variable( "X-averaged negative particle concentration", domain="negative particle", auxiliary_domains={ - "secondary": "negative particle size domain", + "secondary": "negative particle-size domain", #"tertiary": "negative electrode", } ) @@ -82,7 +82,7 @@ def f_a_dist_p(R,R_av_a,sd_a): "X-averaged positive particle concentration", domain="positive particle", auxiliary_domains={ - "secondary": "positive particle size domain", + "secondary": "positive particle-size domain", #"tertiary": "positive electrode" } ) @@ -92,7 +92,7 @@ def f_a_dist_p(R,R_av_a,sd_a): ) phi_s_p = pybamm.Variable( "Positive electrode potential" -# domain="positive particle size domain", +# domain="positive particle-size domain", # auxiliary_domains={ # "secondary": "positive electrode", # } @@ -100,13 +100,13 @@ def f_a_dist_p(R,R_av_a,sd_a): # Spatial Variables R_variable_n = pybamm.SpatialVariable( - "negative particle size variable", - domain=["negative particle size domain"], #could add auxiliary domains + "negative particle-size variable", + domain=["negative particle-size domain"], #could add auxiliary domains coord_sys="cartesian" ) R_variable_p = pybamm.SpatialVariable( - "positive particle size variable", - domain=["positive particle size domain"], #could add auxiliary domains + "positive particle-size variable", + domain=["positive particle-size domain"], #could add auxiliary domains coord_sys="cartesian" ) @@ -123,10 +123,10 @@ def f_a_dist_p(R,R_av_a,sd_a): ###################### # State of Charge ###################### - I = param.dimensional_current_with_time + I_dim = param.dimensional_current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt - self.rhs[Q] = I * param.timescale / 3600 + self.rhs[Q] = I_dim * param.timescale / 3600 # Initial conditions must be provided for the ODEs self.initial_conditions[Q] = pybamm.Scalar(0) @@ -169,8 +169,8 @@ def f_a_dist_p(R,R_av_a,sd_a): R_variable_p ) + i_cell/param.l_p - self.initial_conditions[phi_e] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "negative particle size domain") - self.initial_conditions[phi_s_p] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "positive particle size domain") + self.initial_conditions[phi_e] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "negative particle-size domain") + self.initial_conditions[phi_s_p] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "positive particle-size domain") ###################### # Particles @@ -229,32 +229,43 @@ def f_a_dist_p(R,R_av_a,sd_a): # Primary broadcasts are used to broadcast scalar quantities across a domain # into a vector of the right shape, for multiplying with other vectors + # Time and space output variables + self.set_standard_output_variables() + + # Dimensionless output variables (not already defined) V = phi_s_p + c_e = 1 + # Dimensional output variables V_dim = param.potential_scale * V + (param.U_p_ref - param.U_n_ref) - c_s_surf_n_dim = c_s_surf_n - c_s_surf_p_dim = c_s_surf_p - c_e = 1 - c_e_dim = c_e - phi_s_n_dim = phi_s_n - phi_s_p_dim = phi_s_p - phi_e_dim = phi_e + c_s_n_dim = c_s_n * param.c_n_max + c_s_p_dim = c_s_p * param.c_p_max + c_s_surf_n_dim = c_s_surf_n * param.c_n_max + c_s_surf_p_dim = c_s_surf_p * param.c_p_max + + c_e_dim = c_e * param.c_e_typ + phi_s_n_dim = phi_s_n * param.potential_scale + phi_s_p_dim = phi_s_p * param.potential_scale + (param.U_p_ref - param.U_n_ref) + phi_e_dim = phi_e * param.potential_scale - param.U_n_ref whole_cell = ["negative electrode", "separator", "positive electrode"] - self.variables = { + self.variables.update({ + "Negative particle concentration": c_s_n, + "Negative particle concentration [mol.m-3]": c_s_n_dim, "Negative particle surface concentration": c_s_surf_n, "Negative particle surface concentration [mol.m-3]": c_s_surf_n_dim, "Electrolyte concentration": pybamm.PrimaryBroadcast(c_e, whole_cell), "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast(c_e_dim, whole_cell), + "Positive particle concentration": c_s_p, + "Positive particle concentration [mol.m-3]": c_s_p_dim, "Positive particle surface concentration": c_s_surf_p, "Positive particle surface concentration [mol.m-3]": c_s_surf_p_dim, - "Current [A]": I, "Negative electrode potential": pybamm.PrimaryBroadcast( phi_s_n, "negative electrode" ), @@ -269,9 +280,11 @@ def f_a_dist_p(R,R_av_a,sd_a): "Positive electrode potential [V]": pybamm.PrimaryBroadcast( phi_s_p_dim, "positive electrode" ), + "Current": i_cell, + "Current [A]": I_dim, "Terminal voltage": V, "Terminal voltage [V]": V_dim, - } + }) self.events += [ @@ -279,6 +292,57 @@ def f_a_dist_p(R,R_av_a,sd_a): pybamm.Event("Maximum voltage", V - param.voltage_high_cut), ] + def set_standard_output_variables(self): + # This overwrites the method in parent class, base_lithium_ion_model.BaseModel + + # Time + self.variables.update( + { + "Time": pybamm.t, + "Time [s]": pybamm.t * self.timescale, + "Time [min]": pybamm.t * self.timescale / 60, + "Time [h]": pybamm.t * self.timescale / 3600, + } + ) + + # Spatial + var = pybamm.standard_spatial_vars + L_x = pybamm.geometric_parameters.L_x + self.variables.update( + { + "x": var.x, + "x [m]": var.x * L_x, + "x_n": var.x_n, + "x_n [m]": var.x_n * L_x, + "x_s": var.x_s, + "x_s [m]": var.x_s * L_x, + "x_p": var.x_p, + "x_p [m]": var.x_p * L_x, + } + ) + + # New Spatial Variables + R_variable_n = pybamm.SpatialVariable( + "negative particle-size variable", + domain=["negative particle-size domain"], + coord_sys="cartesian" + ) + R_variable_p = pybamm.SpatialVariable( + "positive particle-size variable", + domain=["positive particle-size domain"], + coord_sys="cartesian" + ) + R_n = pybamm.geometric_parameters.R_n + R_p = pybamm.geometric_parameters.R_p + + self.variables.update( + { + "Negative particle size": R_variable_n, + "Negative particle size [m]": R_variable_n * R_n, + "Positive particle size": R_variable_p, + "Positive particle size [m]": R_variable_p * R_p, + } + ) #################### # Overwrite defaults @@ -292,28 +356,29 @@ def default_parameter_values(self): # append new parameter values - # lognormal area-weighted particle size distribution - def lognormal_dist(R,R_av,sd): + # lognormal area-weighted particle-size distribution + def lognormal_distribution(R,R_av,sd): import numpy as np # inputs are particle radius R, the mean R_av, and standard deviation sd + # inputs can be dimensional or dimensionless mu_ln = pybamm.log(R_av**2/pybamm.sqrt(R_av**2+sd**2)) sigma_ln = pybamm.sqrt(pybamm.log(1 + sd**2/R_av**2)) return pybamm.exp(-(pybamm.log(R)-mu_ln)**2/(2*sigma_ln**2))/pybamm.sqrt(2*np.pi*sigma_ln**2)/R default_params.update( - {"negative area-weighted particle size standard deviation": 0.3}, + {"negative area-weighted particle-size standard deviation": 0.3}, check_already_exists=False ) default_params.update( - {"positive area-weighted particle size standard deviation": 0.3}, + {"positive area-weighted particle-size standard deviation": 0.3}, check_already_exists=False ) default_params.update( - {"negative area-weighted particle size distribution": lognormal_dist}, + {"negative area-weighted particle-size distribution": lognormal_distribution}, check_already_exists=False ) default_params.update( - {"positive area-weighted particle size distribution": lognormal_dist}, + {"positive area-weighted particle-size distribution": lognormal_distribution}, check_already_exists=False ) @@ -325,26 +390,26 @@ def default_geometry(self): # New Spatial Variables R_variable_n = pybamm.SpatialVariable( - "negative particle size variable", - domain=["negative particle size domain"], + "negative particle-size variable", + domain=["negative particle-size domain"], coord_sys="cartesian" ) R_variable_p = pybamm.SpatialVariable( - "positive particle size variable", - domain=["positive particle size domain"], + "positive particle-size variable", + domain=["positive particle-size domain"], coord_sys="cartesian" ) # append new domains default_geom.update( { - "negative particle size domain": { + "negative particle-size domain": { R_variable_n: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(5), } }, - "positive particle size domain": { + "positive particle-size domain": { R_variable_p: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(5), @@ -360,13 +425,13 @@ def default_var_pts(self): # New Spatial Variables R_variable_n = pybamm.SpatialVariable( - "negative particle size variable", - domain=["negative particle size domain"], + "negative particle-size variable", + domain=["negative particle-size domain"], coord_sys="cartesian" ) R_variable_p = pybamm.SpatialVariable( - "positive particle size variable", - domain=["positive particle size domain"], + "positive particle-size variable", + domain=["positive particle-size domain"], coord_sys="cartesian" ) # add to dictionary @@ -384,8 +449,8 @@ def default_submesh_types(self): default_submeshes.update( { - "negative particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "positive particle size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "negative particle-size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "positive particle-size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), } ) return default_submeshes @@ -396,8 +461,8 @@ def default_spatial_methods(self): default_spatials.update( { - "negative particle size domain": pybamm.FiniteVolume(), - "positive particle size domain": pybamm.FiniteVolume(), + "negative particle-size domain": pybamm.FiniteVolume(), + "positive particle-size domain": pybamm.FiniteVolume(), } ) return default_spatials From 86a21b5b9e2a99fee7ba597c0b8aa1ef493f7f36 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 24 Jun 2020 10:44:10 +0100 Subject: [PATCH 04/67] renamed PSD model file --- pybamm/models/full_battery_models/lithium_ion/__init__.py | 2 +- .../lithium_ion/{basic_mpm.py => basic_psd_model.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pybamm/models/full_battery_models/lithium_ion/{basic_mpm.py => basic_psd_model.py} (100%) diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/pybamm/models/full_battery_models/lithium_ion/__init__.py index 0eecff6942..04aedd37f6 100644 --- a/pybamm/models/full_battery_models/lithium_ion/__init__.py +++ b/pybamm/models/full_battery_models/lithium_ion/__init__.py @@ -7,4 +7,4 @@ from .dfn import DFN from .basic_dfn import BasicDFN from .basic_spm import BasicSPM -from .basic_mpm import BasicPSDModel +from .basic_psd_model import BasicPSDModel diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/basic_mpm.py rename to pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py From 1360cc5bffac0ca2a77026ec5cd8ff7fde74bb84 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 24 Jun 2020 15:08:15 +0100 Subject: [PATCH 05/67] added interp for 1D ProcessedVariables in particle-size domain R --- .../lithium_ion/basic_psd_model.py | 58 ++++++++++++++++--- pybamm/solvers/processed_variable.py | 24 +++++--- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py index 19a90e2966..e006712e6a 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py @@ -62,6 +62,13 @@ def f_a_dist_p(R,R_av_a,sd_a): inputs, ) + # Set length scales for additional domains (particle-size domains) + self.length_scales.update( + { + "negative particle-size domain": param.R_n, + "positive particle-size domain": param.R_p, + } + ) ###################### # Variables @@ -236,7 +243,22 @@ def f_a_dist_p(R,R_av_a,sd_a): V = phi_s_p c_e = 1 - + c_s_n_size_av = pybamm.Integral( + f_a_dist_n(R_variable_n, 1, sd_a_n)*c_s_n, + R_variable_n + ) + c_s_p_size_av = pybamm.Integral( + f_a_dist_p(R_variable_p, 1, sd_a_p)*c_s_p, + R_variable_p + ) + c_s_surf_n_size_av = pybamm.Integral( + f_a_dist_n(R_variable_n, 1, sd_a_n)*c_s_surf_n, + R_variable_n + ) + c_s_surf_p_size_av = pybamm.Integral( + f_a_dist_p(R_variable_p, 1, sd_a_p)*c_s_surf_p, + R_variable_p + ) # Dimensional output variables V_dim = param.potential_scale * V + (param.U_p_ref - param.U_n_ref) @@ -245,6 +267,12 @@ def f_a_dist_p(R,R_av_a,sd_a): c_s_surf_n_dim = c_s_surf_n * param.c_n_max c_s_surf_p_dim = c_s_surf_p * param.c_p_max + c_s_n_size_av_dim = c_s_n_size_av * param.c_n_max + c_s_p_size_av_dim = c_s_p_size_av * param.c_p_max + c_s_surf_n_size_av_dim = c_s_surf_n_size_av * param.c_n_max + c_s_surf_p_size_av_dim = c_s_surf_p_size_av * param.c_p_max + + c_e_dim = c_e * param.c_e_typ phi_s_n_dim = phi_s_n * param.potential_scale phi_s_p_dim = phi_s_p * param.potential_scale + (param.U_p_ref - param.U_n_ref) @@ -256,16 +284,28 @@ def f_a_dist_p(R,R_av_a,sd_a): whole_cell = ["negative electrode", "separator", "positive electrode"] self.variables.update({ - "Negative particle concentration": c_s_n, - "Negative particle concentration [mol.m-3]": c_s_n_dim, - "Negative particle surface concentration": c_s_surf_n, - "Negative particle surface concentration [mol.m-3]": c_s_surf_n_dim, + # New "Distribution" variables, those depending on R_variable_n, R_variable_p + "Negative particle concentration distribution": c_s_n, + "Negative particle concentration distribution [mol.m-3]": c_s_n_dim, + "Negative particle surface concentration distribution": c_s_surf_n, + "Negative particle surface concentration distribution [mol.m-3]": c_s_surf_n_dim, + "Positive particle concentration distribution": c_s_p, + "Positive particle concentration distribution [mol.m-3]": c_s_p_dim, + "Positive particle surface concentration distribution": c_s_surf_p, + "Positive particle surface concentration distribution [mol.m-3]": c_s_surf_p_dim, + + + # Standard output quantities (no PSD) + "Negative particle concentration": c_s_n_size_av, + "Negative particle concentration [mol.m-3]": c_s_n_size_av_dim, + "Negative particle surface concentration": c_s_surf_n_size_av, + "Negative particle surface concentration [mol.m-3]": c_s_surf_n_size_av_dim, "Electrolyte concentration": pybamm.PrimaryBroadcast(c_e, whole_cell), "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast(c_e_dim, whole_cell), - "Positive particle concentration": c_s_p, - "Positive particle concentration [mol.m-3]": c_s_p_dim, - "Positive particle surface concentration": c_s_surf_p, - "Positive particle surface concentration [mol.m-3]": c_s_surf_p_dim, + "Positive particle concentration": c_s_p_size_av, + "Positive particle concentration [mol.m-3]": c_s_p_size_av_dim, + "Positive particle surface concentration": c_s_surf_p_size_av, + "Positive particle surface concentration [mol.m-3]": c_s_surf_p_size_av_dim, "Negative electrode potential": pybamm.PrimaryBroadcast( phi_s_n, "negative electrode" ), diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 8e0096f412..b472aaa9d7 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -211,6 +211,12 @@ def initialise_1D(self, fixed_t=False): elif self.domain == ["current collector"]: self.first_dimension = "z" self.z_sol = space + elif self.domain[0] in [ + "negative particle-size domain", + "positive particle-size domain", + ]: + self.first_dimension = "R" + self.R_sol = space else: self.first_dimension = "x" self.x_sol = space @@ -425,9 +431,9 @@ def interp_fun(input): bounds_error=False, ) - def __call__(self, t=None, x=None, r=None, y=None, z=None, warn=True): + def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): """ - Evaluate the variable at arbitrary *dimensional* t (and x, r, y and/or z), + Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), using interpolation """ # If t is None and there is only one value of time in the soluton (i.e. @@ -449,7 +455,7 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, warn=True): if self.dimensions == 0: out = self._interpolation_function(t) elif self.dimensions == 1: - out = self.call_1D(t, x, r, z) + out = self.call_1D(t, x, r, z, R) elif self.dimensions == 2: out = self.call_2D(t, x, r, y, z) if warn is True and np.isnan(out).any(): @@ -458,15 +464,15 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, warn=True): ) return out - def call_1D(self, t, x, r, z): + def call_1D(self, t, x, r, z, R): "Evaluate a 1D variable" - spatial_var = eval_dimension_name(self.first_dimension, x, r, None, z) + spatial_var = eval_dimension_name(self.first_dimension, x, r, None, z, R) return self._interpolation_function(t, spatial_var) def call_2D(self, t, x, r, y, z): "Evaluate a 2D variable" - first_dim = eval_dimension_name(self.first_dimension, x, r, y, z) - second_dim = eval_dimension_name(self.second_dimension, x, r, y, z) + first_dim = eval_dimension_name(self.first_dimension, x, r, y, z, R) + second_dim = eval_dimension_name(self.second_dimension, x, r, y, z, R) if isinstance(first_dim, np.ndarray): if isinstance(second_dim, np.ndarray) and isinstance(t, np.ndarray): first_dim = first_dim[:, np.newaxis, np.newaxis] @@ -501,7 +507,7 @@ def data(self): return self.entries -def eval_dimension_name(name, x, r, y, z): +def eval_dimension_name(name, x, r, y, z, R): if name == "x": out = x elif name == "r": @@ -510,6 +516,8 @@ def eval_dimension_name(name, x, r, y, z): out = y elif name == "z": out = z + elif name == "R": + out = R if out is None: raise ValueError("inputs {} cannot be None".format(name)) From 274413c6e4eab791747e3096b3844a4f24270624 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 25 Jun 2020 18:53:46 +0100 Subject: [PATCH 06/67] fixed style to black --- .../lithium_ion/basic_psd_model.py | 322 ++++++++++-------- pybamm/solvers/processed_variable.py | 4 +- 2 files changed, 176 insertions(+), 150 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py index e006712e6a..e47886aabf 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py @@ -20,7 +20,7 @@ class BasicPSDModel(BaseModel): ---------- .. [1] TL Kirk, J Evans, CP Please and SJ Chapman. “Modelling electrode heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size distributions”. - In: arXiv preprint arXiv:????? (2020). + In: arXiv preprint arXiv:2006.12208 (2020). **Extends:** :class:`pybamm.lithium_ion.BaseModel` @@ -37,29 +37,32 @@ def __init__(self, name="Particle-Size Distribution Model"): # Additional parameters for this model # Dimensionless standard deviations - sd_a_n = pybamm.Parameter("negative area-weighted particle-size standard deviation") - sd_a_p = pybamm.Parameter("positive area-weighted particle-size standard deviation") + sd_a_n = pybamm.Parameter( + "negative area-weighted particle-size standard deviation" + ) + sd_a_p = pybamm.Parameter( + "positive area-weighted particle-size standard deviation" + ) # Particle-size distributions (area-weighted) - def f_a_dist_n(R,R_av_a,sd_a): + def f_a_dist_n(R, R_av_a, sd_a): inputs = { - "negative particle-size variable": R, - "negative area-weighted mean particle size": R_av_a, - "negative area-weighted particle-size standard deviation": sd_a, + "negative particle-size variable": R, + "negative area-weighted mean particle size": R_av_a, + "negative area-weighted particle-size standard deviation": sd_a, } return pybamm.FunctionParameter( - "negative area-weighted particle-size distribution", - inputs, + "negative area-weighted particle-size distribution", inputs, ) - def f_a_dist_p(R,R_av_a,sd_a): + + def f_a_dist_p(R, R_av_a, sd_a): inputs = { - "positive particle-size variable": R, - "positive area-weighted mean particle size": R_av_a, - "positive area-weighted particle-size standard deviation": sd_a, + "positive particle-size variable": R, + "positive area-weighted mean particle size": R_av_a, + "positive area-weighted particle-size standard deviation": sd_a, } return pybamm.FunctionParameter( - "positive area-weighted particle-size distribution", - inputs, + "positive area-weighted particle-size distribution", inputs, ) # Set length scales for additional domains (particle-size domains) @@ -75,47 +78,45 @@ def f_a_dist_p(R,R_av_a,sd_a): ###################### # Discharge capacity Q = pybamm.Variable("Discharge capacity [A.h]") - # X-averaged particle concentrations: these now depend continuously on particle size, and so - # have secondary domains "negative/positive particle-size domain" + # X-averaged particle concentrations: these now depend continuously on particle + # size with secondary domains "negative/positive particle-size domain" c_s_n = pybamm.Variable( "X-averaged negative particle concentration", domain="negative particle", auxiliary_domains={ "secondary": "negative particle-size domain", - #"tertiary": "negative electrode", - } + # "tertiary": "negative electrode", + }, ) c_s_p = pybamm.Variable( "X-averaged positive particle concentration", domain="positive particle", auxiliary_domains={ "secondary": "positive particle-size domain", - #"tertiary": "positive electrode" - } + # "tertiary": "positive electrode" + }, ) # Electrode potentials (leave them without a domain for now) - phi_e = pybamm.Variable( - "Electrolyte potential" - ) + phi_e = pybamm.Variable("Electrolyte potential") phi_s_p = pybamm.Variable( "Positive electrode potential" -# domain="positive particle-size domain", -# auxiliary_domains={ -# "secondary": "positive electrode", -# } + # domain="positive particle-size domain", + # auxiliary_domains={ + # "secondary": "positive electrode", + # } ) # Spatial Variables R_variable_n = pybamm.SpatialVariable( "negative particle-size variable", - domain=["negative particle-size domain"], #could add auxiliary domains - coord_sys="cartesian" - ) + domain=["negative particle-size domain"], # could add auxiliary domains + coord_sys="cartesian", + ) R_variable_p = pybamm.SpatialVariable( "positive particle-size variable", - domain=["positive particle-size domain"], #could add auxiliary domains - coord_sys="cartesian" - ) + domain=["positive particle-size domain"], # could add auxiliary domains + coord_sys="cartesian", + ) # Constant temperature T = param.T_init @@ -144,40 +145,41 @@ def f_a_dist_p(R,R_av_a,sd_a): c_s_surf_n = pybamm.surf(c_s_n) phi_s_n = 0 - - j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n # setting c_e = 1 + j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n # set c_e = 1 j_n = ( 2 * j0_n - * pybamm.sinh( - param.ne_n / 2 * (phi_s_n - phi_e - param.U_n(c_s_surf_n, T)) - ) + * pybamm.sinh(param.ne_n / 2 * (phi_s_n - phi_e - param.U_n(c_s_surf_n, T))) ) c_s_surf_p = pybamm.surf(c_s_p) - j0_p = param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p # setting c_e = 1 + j0_p = ( + param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p + ) # setting c_e = 1 j_p = ( 2 * j0_p - * pybamm.sinh( - param.ne_p / 2 * (phi_s_p - phi_e - param.U_p(c_s_surf_p, T)) - ) + * pybamm.sinh(param.ne_p / 2 * (phi_s_p - phi_e - param.U_p(c_s_surf_p, T))) ) # integral equation for phi_e - self.algebraic[phi_e] = pybamm.Integral( - f_a_dist_n(R_variable_n, 1, sd_a_n)*j_n, - R_variable_n - ) - i_cell/param.l_n + self.algebraic[phi_e] = ( + pybamm.Integral(f_a_dist_n(R_variable_n, 1, sd_a_n) * j_n, R_variable_n) + - i_cell / param.l_n + ) # integral equation for phi_s_p - self.algebraic[phi_s_p] = pybamm.Integral( - f_a_dist_p(R_variable_p, 1, sd_a_p)*j_p, - R_variable_p - ) + i_cell/param.l_p + self.algebraic[phi_s_p] = ( + pybamm.Integral(f_a_dist_p(R_variable_p, 1, sd_a_p) * j_p, R_variable_p) + + i_cell / param.l_p + ) - self.initial_conditions[phi_e] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "negative particle-size domain") - self.initial_conditions[phi_s_p] = pybamm.Scalar(1)#pybamm.PrimaryBroadcast(1, "positive particle-size domain") + self.initial_conditions[phi_e] = pybamm.Scalar( + 1 + ) # pybamm.PrimaryBroadcast(1, "negative particle-size domain") + self.initial_conditions[phi_s_p] = pybamm.Scalar( + 1 + ) # pybamm.PrimaryBroadcast(1, "positive particle-size domain") ###################### # Particles @@ -187,8 +189,8 @@ def f_a_dist_p(R,R_av_a,sd_a): # multiplication at the discretisation stage N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n) N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p) - self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n) / R_variable_n**2 - self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p) / R_variable_p**2 + self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n) / R_variable_n ** 2 + self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p) / R_variable_p ** 2 # Boundary conditions must be provided for equations with spatial derivatives self.boundary_conditions[c_s_n] = { @@ -201,7 +203,12 @@ def f_a_dist_p(R,R_av_a,sd_a): self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -R_variable_p * param.C_p * j_p / param.a_p / param.gamma_p / param.D_p(c_s_surf_p, T), + -R_variable_p + * param.C_p + * j_p + / param.a_p + / param.gamma_p + / param.D_p(c_s_surf_p, T), "Neumann", ), } @@ -230,7 +237,6 @@ def f_a_dist_p(R,R_av_a,sd_a): ), ] - # The `variables` dictionary contains all variables that might be useful for # visualising the solution of the model # Primary broadcasts are used to broadcast scalar quantities across a domain @@ -244,20 +250,16 @@ def f_a_dist_p(R,R_av_a,sd_a): c_e = 1 c_s_n_size_av = pybamm.Integral( - f_a_dist_n(R_variable_n, 1, sd_a_n)*c_s_n, - R_variable_n + f_a_dist_n(R_variable_n, 1, sd_a_n) * c_s_n, R_variable_n ) c_s_p_size_av = pybamm.Integral( - f_a_dist_p(R_variable_p, 1, sd_a_p)*c_s_p, - R_variable_p + f_a_dist_p(R_variable_p, 1, sd_a_p) * c_s_p, R_variable_p ) c_s_surf_n_size_av = pybamm.Integral( - f_a_dist_n(R_variable_n, 1, sd_a_n)*c_s_surf_n, - R_variable_n + f_a_dist_n(R_variable_n, 1, sd_a_n) * c_s_surf_n, R_variable_n ) c_s_surf_p_size_av = pybamm.Integral( - f_a_dist_p(R_variable_p, 1, sd_a_p)*c_s_surf_p, - R_variable_p + f_a_dist_p(R_variable_p, 1, sd_a_p) * c_s_surf_p, R_variable_p ) # Dimensional output variables V_dim = param.potential_scale * V + (param.U_p_ref - param.U_n_ref) @@ -272,60 +274,65 @@ def f_a_dist_p(R,R_av_a,sd_a): c_s_surf_n_size_av_dim = c_s_surf_n_size_av * param.c_n_max c_s_surf_p_size_av_dim = c_s_surf_p_size_av * param.c_p_max - c_e_dim = c_e * param.c_e_typ phi_s_n_dim = phi_s_n * param.potential_scale phi_s_p_dim = phi_s_p * param.potential_scale + (param.U_p_ref - param.U_n_ref) - phi_e_dim = phi_e * param.potential_scale - param.U_n_ref - - - + phi_e_dim = phi_e * param.potential_scale - param.U_n_ref whole_cell = ["negative electrode", "separator", "positive electrode"] - self.variables.update({ - # New "Distribution" variables, those depending on R_variable_n, R_variable_p - "Negative particle concentration distribution": c_s_n, - "Negative particle concentration distribution [mol.m-3]": c_s_n_dim, - "Negative particle surface concentration distribution": c_s_surf_n, - "Negative particle surface concentration distribution [mol.m-3]": c_s_surf_n_dim, - "Positive particle concentration distribution": c_s_p, - "Positive particle concentration distribution [mol.m-3]": c_s_p_dim, - "Positive particle surface concentration distribution": c_s_surf_p, - "Positive particle surface concentration distribution [mol.m-3]": c_s_surf_p_dim, - - - # Standard output quantities (no PSD) - "Negative particle concentration": c_s_n_size_av, - "Negative particle concentration [mol.m-3]": c_s_n_size_av_dim, - "Negative particle surface concentration": c_s_surf_n_size_av, - "Negative particle surface concentration [mol.m-3]": c_s_surf_n_size_av_dim, - "Electrolyte concentration": pybamm.PrimaryBroadcast(c_e, whole_cell), - "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast(c_e_dim, whole_cell), - "Positive particle concentration": c_s_p_size_av, - "Positive particle concentration [mol.m-3]": c_s_p_size_av_dim, - "Positive particle surface concentration": c_s_surf_p_size_av, - "Positive particle surface concentration [mol.m-3]": c_s_surf_p_size_av_dim, - "Negative electrode potential": pybamm.PrimaryBroadcast( - phi_s_n, "negative electrode" - ), - "Negative electrode potential [V]": pybamm.PrimaryBroadcast( - phi_s_n_dim, "negative electrode" - ), - "Electrolyte potential": pybamm.PrimaryBroadcast(phi_e, whole_cell), - "Electrolyte potential [V]": pybamm.PrimaryBroadcast(phi_e_dim, whole_cell), - "Positive electrode potential": pybamm.PrimaryBroadcast( - phi_s_p, "positive electrode" - ), - "Positive electrode potential [V]": pybamm.PrimaryBroadcast( - phi_s_p_dim, "positive electrode" - ), - "Current": i_cell, - "Current [A]": I_dim, - "Terminal voltage": V, - "Terminal voltage [V]": V_dim, - }) - + self.variables.update( + { + # (Some of) New "Distribution" variables, those depending on R_variable_n, R_variable_p + "Negative particle concentration distribution": c_s_n, + "Negative particle concentration distribution [mol.m-3]": c_s_n_dim, + "Negative particle surface concentration distribution": c_s_surf_n, + "Negative particle surface concentration distribution [mol.m-3]": c_s_surf_n_dim, + "Positive particle concentration distribution": c_s_p, + "Positive particle concentration distribution [mol.m-3]": c_s_p_dim, + "Positive particle surface concentration distribution": c_s_surf_p, + "Positive particle surface concentration distribution [mol.m-3]": c_s_surf_p_dim, + "Negative area-weighted particle-size distribution": f_a_dist_n( + R_variable_n, 1, sd_a_n + ), + "Positive area-weighted particle-size distribution": f_a_dist_p( + R_variable_p, 1, sd_a_p + ), + # Standard output quantities (no PSD) + "Negative particle concentration": c_s_n_size_av, + "Negative particle concentration [mol.m-3]": c_s_n_size_av_dim, + "Negative particle surface concentration": c_s_surf_n_size_av, + "Negative particle surface concentration [mol.m-3]": c_s_surf_n_size_av_dim, + "Electrolyte concentration": pybamm.PrimaryBroadcast(c_e, whole_cell), + "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast( + c_e_dim, whole_cell + ), + "Positive particle concentration": c_s_p_size_av, + "Positive particle concentration [mol.m-3]": c_s_p_size_av_dim, + "Positive particle surface concentration": c_s_surf_p_size_av, + "Positive particle surface concentration [mol.m-3]": c_s_surf_p_size_av_dim, + "Negative electrode potential": pybamm.PrimaryBroadcast( + phi_s_n, "negative electrode" + ), + "Negative electrode potential [V]": pybamm.PrimaryBroadcast( + phi_s_n_dim, "negative electrode" + ), + "Electrolyte potential": pybamm.PrimaryBroadcast(phi_e, whole_cell), + "Electrolyte potential [V]": pybamm.PrimaryBroadcast( + phi_e_dim, whole_cell + ), + "Positive electrode potential": pybamm.PrimaryBroadcast( + phi_s_p, "positive electrode" + ), + "Positive electrode potential [V]": pybamm.PrimaryBroadcast( + phi_s_p_dim, "positive electrode" + ), + "Current": i_cell, + "Current [A]": I_dim, + "Terminal voltage": V, + "Terminal voltage [V]": V_dim, + } + ) self.events += [ pybamm.Event("Minimum voltage", V - param.voltage_low_cut), @@ -365,13 +372,13 @@ def set_standard_output_variables(self): R_variable_n = pybamm.SpatialVariable( "negative particle-size variable", domain=["negative particle-size domain"], - coord_sys="cartesian" - ) + coord_sys="cartesian", + ) R_variable_p = pybamm.SpatialVariable( "positive particle-size variable", domain=["positive particle-size domain"], - coord_sys="cartesian" - ) + coord_sys="cartesian", + ) R_n = pybamm.geometric_parameters.R_n R_p = pybamm.geometric_parameters.R_p @@ -393,33 +400,53 @@ def default_parameter_values(self): # Lion parameters left as default parameter set for tests default_params = super().default_parameter_values - - # append new parameter values - + # New parameter values + # Area-weighted standard deviations (dimensionless) + sd_a_n = 0.5 + sd_a_p = 0.3 + # Max radius in the particle-size distribution (dimensionless) + R_n_max = max(2, 1 + sd_a_n * 5) + R_p_max = max(2, 1 + sd_a_p * 5) # lognormal area-weighted particle-size distribution - def lognormal_distribution(R,R_av,sd): + + def lognormal_distribution(R, R_av, sd): import numpy as np + # inputs are particle radius R, the mean R_av, and standard deviation sd # inputs can be dimensional or dimensionless - mu_ln = pybamm.log(R_av**2/pybamm.sqrt(R_av**2+sd**2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd**2/R_av**2)) - return pybamm.exp(-(pybamm.log(R)-mu_ln)**2/(2*sigma_ln**2))/pybamm.sqrt(2*np.pi*sigma_ln**2)/R + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / R + ) default_params.update( - {"negative area-weighted particle-size standard deviation": 0.3}, - check_already_exists=False + {"negative area-weighted particle-size standard deviation": sd_a_n}, + check_already_exists=False, ) default_params.update( - {"positive area-weighted particle-size standard deviation": 0.3}, - check_already_exists=False + {"positive area-weighted particle-size standard deviation": sd_a_p}, + check_already_exists=False, ) default_params.update( - {"negative area-weighted particle-size distribution": lognormal_distribution}, - check_already_exists=False + {"negative maximum particle radius": R_n_max}, check_already_exists=False ) default_params.update( - {"positive area-weighted particle-size distribution": lognormal_distribution}, - check_already_exists=False + {"positive maximum particle radius": R_p_max}, check_already_exists=False + ) + default_params.update( + { + "negative area-weighted particle-size distribution": lognormal_distribution + }, + check_already_exists=False, + ) + default_params.update( + { + "positive area-weighted particle-size distribution": lognormal_distribution + }, + check_already_exists=False, ) return default_params @@ -432,13 +459,13 @@ def default_geometry(self): R_variable_n = pybamm.SpatialVariable( "negative particle-size variable", domain=["negative particle-size domain"], - coord_sys="cartesian" - ) + coord_sys="cartesian", + ) R_variable_p = pybamm.SpatialVariable( "positive particle-size variable", domain=["positive particle-size domain"], - coord_sys="cartesian" - ) + coord_sys="cartesian", + ) # append new domains default_geom.update( @@ -446,13 +473,13 @@ def default_geometry(self): "negative particle-size domain": { R_variable_n: { "min": pybamm.Scalar(0), - "max": pybamm.Scalar(5), + "max": pybamm.Parameter("negative maximum particle radius"), } }, "positive particle-size domain": { R_variable_p: { "min": pybamm.Scalar(0), - "max": pybamm.Scalar(5), + "max": pybamm.Parameter("negative maximum particle radius"), } }, } @@ -467,20 +494,15 @@ def default_var_pts(self): R_variable_n = pybamm.SpatialVariable( "negative particle-size variable", domain=["negative particle-size domain"], - coord_sys="cartesian" + coord_sys="cartesian", ) R_variable_p = pybamm.SpatialVariable( "positive particle-size variable", domain=["positive particle-size domain"], - coord_sys="cartesian" + coord_sys="cartesian", ) # add to dictionary - defaults.update( - { - R_variable_n: 50, - R_variable_p: 50, - } - ) + defaults.update({R_variable_n: 50, R_variable_p: 50}) return defaults @property @@ -489,8 +511,12 @@ def default_submesh_types(self): default_submeshes.update( { - "negative particle-size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "positive particle-size domain": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "negative particle-size domain": pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ), + "positive particle-size domain": pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ), } ) return default_submeshes diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index b472aaa9d7..ff7c9f3913 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -457,7 +457,7 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): elif self.dimensions == 1: out = self.call_1D(t, x, r, z, R) elif self.dimensions == 2: - out = self.call_2D(t, x, r, y, z) + out = self.call_2D(t, x, r, y, z, R) if warn is True and np.isnan(out).any(): pybamm.logger.warning( "Calling variable outside interpolation range (returns 'nan')" @@ -469,7 +469,7 @@ def call_1D(self, t, x, r, z, R): spatial_var = eval_dimension_name(self.first_dimension, x, r, None, z, R) return self._interpolation_function(t, spatial_var) - def call_2D(self, t, x, r, y, z): + def call_2D(self, t, x, r, y, z, R): "Evaluate a 2D variable" first_dim = eval_dimension_name(self.first_dimension, x, r, y, z, R) second_dim = eval_dimension_name(self.second_dimension, x, r, y, z, R) From 4bbc39b2afd7bdbce84a6b4db2010ef2e4ddd17d Mon Sep 17 00:00:00 2001 From: tobykirk Date: Mon, 13 Jul 2020 16:02:56 +0100 Subject: [PATCH 07/67] added PSDModel incorporating submodel structure --- pybamm/expression_tree/broadcasts.py | 44 ++- pybamm/geometry/standard_spatial_vars.py | 30 ++ .../full_battery_models/base_battery_model.py | 1 + .../lithium_ion/__init__.py | 1 + .../lithium_ion/basic_psd_model.py | 3 +- .../lithium_ion/psd_model.py | 308 ++++++++++++++++++ .../submodels/electrode/ohm/__init__.py | 1 + .../ohm/leading_size_distribution_ohm.py | 161 +++++++++ .../submodels/interface/base_interface.py | 99 +++++- .../interface/kinetics/base_kinetics.py | 86 ++++- pybamm/models/submodels/particle/__init__.py | 1 + ...ckian_single_particle_size_distribution.py | 264 +++++++++++++++ pybamm/parameters/geometric_parameters.py | 10 + .../standard_parameters_lithium_ion.py | 46 +++ 14 files changed, 1034 insertions(+), 21 deletions(-) create mode 100644 pybamm/models/full_battery_models/lithium_ion/psd_model.py create mode 100644 pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py create mode 100644 pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py diff --git a/pybamm/expression_tree/broadcasts.py b/pybamm/expression_tree/broadcasts.py index fa13b74bc7..75d899fab0 100644 --- a/pybamm/expression_tree/broadcasts.py +++ b/pybamm/expression_tree/broadcasts.py @@ -92,29 +92,44 @@ def check_and_set_domains( self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains ): "See :meth:`Broadcast.check_and_set_domains`" - # Can only do primary broadcast from current collector to electrode or particle - # or from electrode to particle. Note current collector to particle *is* allowed + # Can only do primary broadcast from current collector to electrode, particle-size or particle + # or from electrode to particle-size or particle. Note e.g. current collector to particle *is* allowed if child.domain == []: pass elif child.domain == ["current collector"] and broadcast_domain[0] not in [ "negative electrode", "separator", "positive electrode", + "negative particle-size domain", + "positive particle-size domain", "negative particle", "positive particle", ]: raise pybamm.DomainError( """Primary broadcast from current collector domain must be to electrode - or separator or particle domains""" + or separator or particle or particle-size domains""" ) elif child.domain[0] in [ "negative electrode", "separator", "positive electrode", - ] and broadcast_domain[0] not in ["negative particle", "positive particle"]: + ] and broadcast_domain[0] not in [ + "negative particle", + "positive particle", + "negative particle-size domain", + "positive particle-size domain", + ]: raise pybamm.DomainError( """Primary broadcast from electrode or separator must be to particle - domains""" + or particle-size domains""" + ) + elif child.domain[0] in [ + "negative particle-size domain", + "positive particle-size domain", + ] and broadcast_domain[0] not in ["negative particle", "positive particle",]: + raise pybamm.DomainError( + """Primary broadcast from particle-size domain must be to particle + domain""" ) elif child.domain[0] in ["negative particle", "positive particle"]: raise pybamm.DomainError("Cannot do primary broadcast from particle domain") @@ -190,14 +205,29 @@ def check_and_set_domains( if child.domain[0] in [ "negative particle", "positive particle", + ] and broadcast_domain[0] not in [ + "negative particle-size domain", + "positive particle-size domain", + "negative electrode", + "separator", + "positive electrode", + ]: + raise pybamm.DomainError( + """Secondary broadcast from particle domain must be to particle-size, + electrode or separator domains""" + ) + if child.domain[0] in [ + "negative particle-size domain", + "positive particle-size domain", ] and broadcast_domain[0] not in [ "negative electrode", "separator", "positive electrode", + "current collector" ]: raise pybamm.DomainError( - """Secondary broadcast from particle domain must be to electrode or - separator domains""" + """Secondary broadcast from particle-size domain must be to + electrode or separator or current collector domains""" ) elif child.domain[0] in [ "negative electrode", diff --git a/pybamm/geometry/standard_spatial_vars.py b/pybamm/geometry/standard_spatial_vars.py index 8f547ef6da..f040c968fd 100644 --- a/pybamm/geometry/standard_spatial_vars.py +++ b/pybamm/geometry/standard_spatial_vars.py @@ -50,6 +50,25 @@ coord_sys="spherical polar", ) +R_variable_n = pybamm.SpatialVariable( + "negative particle-size variable", + domain=["negative particle-size domain"], + # auxiliary_domains={ + # "secondary": "negative electrode", + # "tertiary": "current collector", + # }, + coord_sys="cartesian", +) +R_variable_p = pybamm.SpatialVariable( + "positive particle-size variable", + domain=["positive particle-size domain"], + # auxiliary_domains={ + # "secondary": "positive electrode", + # "tertiary": "current collector", + # }, + coord_sys="cartesian", +) + # Domains at cell edges x_n_edge = pybamm.SpatialVariableEdge( "x_n", @@ -101,3 +120,14 @@ }, coord_sys="spherical polar", ) + +R_variable_n_edge = pybamm.SpatialVariableEdge( + "negative particle-size variable", + domain=["negative particle-size domain"], + coord_sys="cartesian", +) +R_variable_p_edge = pybamm.SpatialVariableEdge( + "positive particle-size variable", + domain=["positive particle-size domain"], + coord_sys="cartesian", +) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index c8da89e922..1386333bde 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -184,6 +184,7 @@ def options(self, extra_options): "interfacial surface area": "constant", "current collector": "uniform", "particle": "Fickian diffusion", + "particle-size distribution": False, "thermal": "isothermal", "cell_geometry": None, "external submodels": [], diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/pybamm/models/full_battery_models/lithium_ion/__init__.py index 04aedd37f6..dd87822da6 100644 --- a/pybamm/models/full_battery_models/lithium_ion/__init__.py +++ b/pybamm/models/full_battery_models/lithium_ion/__init__.py @@ -8,3 +8,4 @@ from .basic_dfn import BasicDFN from .basic_spm import BasicSPM from .basic_psd_model import BasicPSDModel +from .psd_model import PSDModel diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py index e47886aabf..09ee87d77e 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py @@ -340,7 +340,8 @@ def f_a_dist_p(R, R_av_a, sd_a): ] def set_standard_output_variables(self): - # This overwrites the method in parent class, base_lithium_ion_model.BaseModel + # This overwrites the method in parent class, base_lithium_ion_model.BaseModel, + # adding "particle-size variables" R_variable_n and R_variable_p # Time self.variables.update( diff --git a/pybamm/models/full_battery_models/lithium_ion/psd_model.py b/pybamm/models/full_battery_models/lithium_ion/psd_model.py new file mode 100644 index 0000000000..3d5ec3fc9e --- /dev/null +++ b/pybamm/models/full_battery_models/lithium_ion/psd_model.py @@ -0,0 +1,308 @@ +# +# Single Particle Model (SPM) +# +import pybamm +from .base_lithium_ion_model import BaseModel + + +class PSDModel(BaseModel): + """Particle-Size Distribution (PSD) Model of a lithium-ion battery, from [1]_. + + Parameters + ---------- + options : dict, optional + A dictionary of options to be passed to the model. + name : str, optional + The name of the model. + build : bool, optional + Whether to build the model on instantiation. Default is True. Setting this + option to False allows users to change any number of the submodels before + building the complete model (submodels cannot be changed after the model is + built). + + References + ---------- + .. [1] TL Kirk, J Evans, CP Please and SJ Chapman. “Modelling electrode heterogeneity + in lithium-ion batteries: unimodal and bimodal particle-size distributions”. + In: arXiv preprint arXiv:2006.12208 (2020). + + **Extends:** :class:`pybamm.lithium_ion.BaseModel` + """ + + def __init__( + self, options=None, name="Particle-Size Distribution Model", build=True + ): + super().__init__(options, name) + self.options["particle-size distribution"] = True + + # Set length scales for additional domains (particle-size domains) + self.length_scales.update( + { + "negative particle-size domain": self.param.R_n, + "positive particle-size domain": self.param.R_p, + } + ) + # Update standard output variables + self.set_standard_output_variables() + + # Set submodels + self.set_external_circuit_submodel() + self.set_porosity_submodel() + self.set_tortuosity_submodels() + self.set_convection_submodel() + self.set_interfacial_submodel() + self.set_other_reaction_submodels_to_zero() + self.set_particle_submodel() + self.set_negative_electrode_submodel() + self.set_electrolyte_submodel() + self.set_positive_electrode_submodel() + self.set_thermal_submodel() + self.set_current_collector_submodel() + self.set_sei_submodel() + + if build: + self.build_model() + + # pybamm.citations.register("marquis2019asymptotic") + + def set_porosity_submodel(self): + + self.submodels["porosity"] = pybamm.porosity.Constant(self.param) + + def set_convection_submodel(self): + + self.submodels[ + "through-cell convection" + ] = pybamm.convection.through_cell.NoConvection(self.param) + self.submodels[ + "transverse convection" + ] = pybamm.convection.transverse.NoConvection(self.param) + + def set_interfacial_submodel(self): + + self.submodels["negative interface"] = pybamm.interface.ButlerVolmer( + self.param, "Negative", "lithium-ion main", self.options + ) + + self.submodels["positive interface"] = pybamm.interface.ButlerVolmer( + self.param, "Positive", "lithium-ion main", self.options + ) + + def set_particle_submodel(self): + + if self.options["particle"] == "Fickian diffusion": + self.submodels["negative particle"] = pybamm.particle.FickianSinglePSD( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FickianSinglePSD( + self.param, "Positive" + ) + elif self.options["particle"] == "fast diffusion": + self.submodels["negative particle"] = pybamm.particle.FastSinglePSD( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FastSinglePSD( + self.param, "Positive" + ) + + def set_negative_electrode_submodel(self): + + self.submodels[ + "negative electrode" + ] = pybamm.electrode.ohm.LeadingOrderSizeDistribution(self.param, "Negative") + + def set_positive_electrode_submodel(self): + + self.submodels[ + "positive electrode" + ] = pybamm.electrode.ohm.LeadingOrderSizeDistribution(self.param, "Positive") + + def set_electrolyte_submodel(self): + + surf_form = pybamm.electrolyte_conductivity.surface_potential_form + + if self.options["surface form"] is False: + self.submodels[ + "leading-order electrolyte conductivity" + ] = pybamm.electrolyte_conductivity.LeadingOrder(self.param) + + elif self.options["surface form"] == "differential": + for domain in ["Negative", "Separator", "Positive"]: + self.submodels[ + "leading-order " + domain.lower() + " electrolyte conductivity" + ] = surf_form.LeadingOrderDifferential(self.param, domain) + + elif self.options["surface form"] == "algebraic": + for domain in ["Negative", "Separator", "Positive"]: + self.submodels[ + "leading-order " + domain.lower() + " electrolyte conductivity" + ] = surf_form.LeadingOrderAlgebraic(self.param, domain) + self.submodels[ + "electrolyte diffusion" + ] = pybamm.electrolyte_diffusion.ConstantConcentration(self.param) + + def set_standard_output_variables(self): + super().set_standard_output_variables() + + # add particle-size variables + var = pybamm.standard_spatial_vars + R_n = pybamm.geometric_parameters.R_n + R_p = pybamm.geometric_parameters.R_p + self.variables.update( + { + "Negative particle size": var.R_variable_n, + "Negative particle size [m]": var.R_variable_n * R_n, + "Positive particle size": var.R_variable_p, + "Positive particle size [m]": var.R_variable_p * R_p, + } + ) + + #################### + # Overwrite defaults + #################### + @property + def default_parameter_values(self): + # Default parameter values + # Lion parameters left as default parameter set for tests + default_params = super().default_parameter_values + + # New parameter values + # Area-weighted standard deviations + sd_a_n = 0.5 + sd_a_p = 0.3 + sd_a_n_dim = sd_a_n * default_params["Negative particle radius [m]"] + sd_a_p_dim = sd_a_p * default_params["Positive particle radius [m]"] + + # Max radius in the particle-size distribution (dimensionless) + R_n_max = max(2, 1 + sd_a_n * 5) + R_p_max = max(2, 1 + sd_a_p * 5) + + # lognormal area-weighted particle-size distribution + + def lognormal_distribution(R, R_av, sd): + import numpy as np + + # inputs are particle radius R, the mean R_av, and standard deviation sd + # inputs can be dimensional or dimensionless + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R + 0.01) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / (R + 0.01) + ) + def lognormal_distribution(R, R_av, sd): + import numpy as np + + # inputs are particle radius R, the mean R_av, and standard deviation sd + # inputs can be dimensional or dimensionless + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / (R) + ) + + default_params.update( + {"Negative area-weighted particle-size standard deviation": sd_a_n}, + check_already_exists=False, + ) + default_params.update( + {"Negative area-weighted particle-size standard deviation [m]": sd_a_n_dim}, + check_already_exists=False, + ) + default_params.update( + {"Positive area-weighted particle-size standard deviation": sd_a_p}, + check_already_exists=False, + ) + default_params.update( + {"Positive area-weighted particle-size standard deviation [m]": sd_a_p_dim}, + check_already_exists=False, + ) + default_params.update( + {"Negative maximum particle radius": R_n_max}, check_already_exists=False + ) + default_params.update( + {"Positive maximum particle radius": R_p_max}, check_already_exists=False + ) + default_params.update( + { + "Negative area-weighted particle-size distribution [m]": lognormal_distribution + }, + check_already_exists=False, + ) + default_params.update( + { + "Positive area-weighted particle-size distribution [m]": lognormal_distribution + }, + check_already_exists=False, + ) + + return default_params + + @property + def default_geometry(self): + default_geom = super().default_geometry + + # New Spatial Variables + R_variable_n = pybamm.standard_spatial_vars.R_variable_n + R_variable_p = pybamm.standard_spatial_vars.R_variable_p + + # append new domains + default_geom.update( + { + "negative particle-size domain": { + R_variable_n: { + "min": pybamm.Scalar(0), + "max": pybamm.Parameter("Negative maximum particle radius"), + } + }, + "positive particle-size domain": { + R_variable_p: { + "min": pybamm.Scalar(0), + "max": pybamm.Parameter("Positive maximum particle radius"), + } + }, + } + ) + return default_geom + + @property + def default_var_pts(self): + defaults = super().default_var_pts + + # New Spatial Variables + R_variable_n = pybamm.standard_spatial_vars.R_variable_n + R_variable_p = pybamm.standard_spatial_vars.R_variable_p + # add to dictionary + defaults.update({R_variable_n: 50, R_variable_p: 50}) + return defaults + + @property + def default_submesh_types(self): + default_submeshes = super().default_submesh_types + + default_submeshes.update( + { + "negative particle-size domain": pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ), + "positive particle-size domain": pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ), + } + ) + return default_submeshes + + @property + def default_spatial_methods(self): + default_spatials = super().default_spatial_methods + + default_spatials.update( + { + "negative particle-size domain": pybamm.FiniteVolume(), + "positive particle-size domain": pybamm.FiniteVolume(), + } + ) + return default_spatials diff --git a/pybamm/models/submodels/electrode/ohm/__init__.py b/pybamm/models/submodels/electrode/ohm/__init__.py index 4d684769f3..c7a788eca3 100644 --- a/pybamm/models/submodels/electrode/ohm/__init__.py +++ b/pybamm/models/submodels/electrode/ohm/__init__.py @@ -2,4 +2,5 @@ from .composite_ohm import Composite from .full_ohm import Full from .leading_ohm import LeadingOrder +from .leading_size_distribution_ohm import LeadingOrderSizeDistribution from .surface_form_ohm import SurfaceForm diff --git a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py new file mode 100644 index 0000000000..5b63f2db87 --- /dev/null +++ b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py @@ -0,0 +1,161 @@ +# +# Full model for Ohm's law in the electrode +# +import pybamm + +from .base_ohm import BaseModel + + +class LeadingOrderSizeDistribution(BaseModel): + """An electrode submodel that employs Ohm's law the leading-order approximation to + governing equations when there is a distribution of particle sizes. An algebraic + equation is imposed for the x-averaged surface potential difference. + + Parameters + ---------- + param : parameter class + 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 positive potential based on the current. + If False, the potential is specified by the user. Default is True. + + **Extends:** :class:`pybamm.electrode.ohm.BaseModel` + """ + + def __init__(self, param, domain, set_positive_potential=True): + super().__init__(param, domain, set_positive_potential=set_positive_potential) + + def get_fundamental_variables(self): + + delta_phi_av = pybamm.Variable( + "X-averaged " + + self.domain.lower() + + " electrode surface potential difference", + domain="current collector", + ) + variables = self._get_standard_surface_potential_difference_variables( + delta_phi_av + ) + + return variables + + def get_coupled_variables(self, variables): + + i_boundary_cc = variables["Current collector current density"] + phi_s_cn = variables["Negative current collector potential"] + + # import parameters and spatial variables + l_n = self.param.l_n + l_p = self.param.l_p + x_n = pybamm.standard_spatial_vars.x_n + x_p = pybamm.standard_spatial_vars.x_p + + if self.domain == "Negative": + phi_s = pybamm.PrimaryBroadcast(phi_s_cn, ["negative electrode"]) + i_s = i_boundary_cc * (1 - x_n / l_n) + + elif self.domain == "Positive": + # recall delta_phi = phi_s - phi_e + delta_phi_p_av = variables[ + "X-averaged positive electrode surface potential difference" + ] + phi_e_p_av = variables["X-averaged positive electrolyte potential"] + + v = delta_phi_p_av + phi_e_p_av + + phi_s = pybamm.PrimaryBroadcast(v, ["positive electrode"]) + i_s = i_boundary_cc * (1 - (1 - x_p) / l_p) + + variables.update(self._get_standard_potential_variables(phi_s)) + variables.update(self._get_standard_current_variables(i_s)) + + if self.domain == "Positive": + variables.update(self._get_standard_whole_cell_variables(variables)) + + return variables + + def set_algebraic(self, variables): + + j_tot_av = variables[ + "X-averaged " + + self.domain.lower() + + " electrode total interfacial current density" + ] + + # Extract total sum of interfacial current densities + sum_j_av = variables[ + "Sum of x-averaged " + + self.domain.lower() + + " electrode interfacial current densities" + ] + delta_phi_av = variables[ + "X-averaged " + + self.domain.lower() + + " electrode surface potential difference" + ] +# i_cell = self.param.current_with_time +# if self.domain == "Negative": +# j_tot_av = i_cell/self.param.l_n +# elif self.domain == "Positive": +# j_tot_av = -i_cell/self.param.l_p + self.algebraic[delta_phi_av] = sum_j_av - j_tot_av#delta_phi_av - 1# + + def set_initial_conditions(self, variables): + + delta_phi_av = variables[ + "X-averaged " + + self.domain.lower() + + " electrode surface potential difference" + ] + T_init = self.param.T_init + + if self.domain == "Negative": + delta_phi_av_init = self.param.U_n(self.param.c_n_init(0), T_init) + elif self.domain == "Positive": + delta_phi_av_init = self.param.U_p( + self.param.c_p_init(1), T_init + ) + + self.initial_conditions[delta_phi_av] = delta_phi_av_init + + def _get_standard_surface_potential_difference_variables(self, delta_phi): + + if self.domain == "Negative": + ocp_ref = self.param.U_n_ref + elif self.domain == "Positive": + ocp_ref = self.param.U_p_ref + pot_scale = self.param.potential_scale + + # Average, and broadcast if necessary + if delta_phi.domain == []: + delta_phi_av = delta_phi + delta_phi = pybamm.FullBroadcast( + delta_phi, self.domain_for_broadcast, "current collector" + ) + elif delta_phi.domain == ["current collector"]: + delta_phi_av = delta_phi + delta_phi = pybamm.PrimaryBroadcast(delta_phi, self.domain_for_broadcast) + else: + delta_phi_av = pybamm.x_average(delta_phi) + + # # For particle-size distributions (true here), must broadcast further + # delta_phi = pybamm.PrimaryBroadcast(delta_phi, [self.domain.lower() + " particle-size domain"]) + # delta_phi_av = pybamm.PrimaryBroadcast(delta_phi_av, [self.domain.lower() + " particle-size domain"]) + + variables = { + self.domain + " electrode surface potential difference": delta_phi, + "X-averaged " + + self.domain.lower() + + " electrode surface potential difference": delta_phi_av, + self.domain + + " electrode surface potential difference [V]": ocp_ref + + delta_phi * pot_scale, + "X-averaged " + + self.domain.lower() + + " electrode surface potential difference [V]": ocp_ref + + delta_phi_av * pot_scale, + } + + return variables diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index ad8bc0420f..53f3013431 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -58,9 +58,16 @@ def _get_exchange_current_density(self, variables): T = variables[self.domain + " electrode temperature"] if self.reaction == "lithium-ion main": - c_s_surf = variables[self.domain + " particle surface concentration"] - - # If variable was broadcast, take only the orphan + # For "particle-size distribution", take distribution version + # of c_s_surf that depends on particle size. + if self.options["particle-size distribution"] == True: + c_s_surf = variables[ + self.domain + " particle surface concentration distribution" + ] + else: + c_s_surf = variables[self.domain + " particle surface concentration"] + + # If all variables were broadcast, take only the orphans if ( isinstance(c_s_surf, pybamm.Broadcast) and isinstance(c_e, pybamm.Broadcast) @@ -69,6 +76,15 @@ def _get_exchange_current_density(self, variables): c_s_surf = c_s_surf.orphans[0] c_e = c_e.orphans[0] T = T.orphans[0] + # For "particle-size distribution", broadcast c_e, T onto particle-size + # domain + if self.options["particle-size distribution"] == True: + c_e = pybamm.PrimaryBroadcast( + c_e, [self.domain.lower() + " particle-size domain"] + ) + T = pybamm.PrimaryBroadcast( + T, [self.domain.lower() + " particle-size domain"] + ) if self.domain == "Negative": j0 = self.param.j0_n(c_e, c_s_surf, T) / self.param.C_r_n elif self.domain == "Positive": @@ -121,16 +137,27 @@ def _get_open_circuit_potential(self, variables): """ if self.reaction == "lithium-ion main": - c_s_surf = variables[self.domain + " particle surface concentration"] T = variables[self.domain + " electrode temperature"] + # For "particle-size distribution" models, take distribution version + # of c_s_surf that depends on particle size. + if self.options["particle-size distribution"] == True: + c_s_surf = variables[ + self.domain + " particle surface concentration distribution" + ] + else: + c_s_surf = variables[self.domain + " particle surface concentration"] # If variable was broadcast, take only the orphan - if isinstance(c_s_surf, pybamm.Broadcast) and isinstance( - T, pybamm.Broadcast + if (isinstance(c_s_surf, pybamm.Broadcast) and isinstance(T, pybamm.Broadcast) ): c_s_surf = c_s_surf.orphans[0] T = T.orphans[0] - + # For "particle-size distribution" models, then broadcast T onto particle-size + # domain + if self.options["particle-size distribution"] == True: + T = pybamm.PrimaryBroadcast( + T, [self.domain.lower() + " particle-size domain"] + ) if self.domain == "Negative": ocp = self.param.U_n(c_s_surf, T) dUdT = self.param.dUdT_n(c_s_surf) @@ -368,7 +395,20 @@ def _get_standard_exchange_current_variables(self, j0): elif self.domain == "Positive": j_scale = i_typ / (self.param.a_p_dim * L_x) - # Average, and broadcast if necessary + # If j0 depends on particle size R then must R-average to get standard + # output exchange current density + if j0.domain == [self.domain.lower() + " particle-size domain"]: + if self.domain == "Negative": + R_variable = pybamm.standard_spatial_vars.R_variable_n + f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + elif self.domain == "Positive": + R_variable = pybamm.standard_spatial_vars.R_variable_p + f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + + # R-average + j0 = pybamm.Integral(f_a_dist * j0, R_variable) + + # X-average, and broadcast if necessary if j0.domain == []: j0_av = j0 j0 = pybamm.FullBroadcast( @@ -449,7 +489,20 @@ def _get_standard_whole_cell_exchange_current_variables(self, variables): def _get_standard_overpotential_variables(self, eta_r): pot_scale = self.param.potential_scale - # Average, and broadcast if necessary + # If eta_r depends on particle size R then must R-average to get standard + # output reaction overpotential + if eta_r.domain == [self.domain.lower() + " particle-size domain"]: + if self.domain == "Negative": + R_variable = pybamm.standard_spatial_vars.R_variable_n + f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + elif self.domain == "Positive": + R_variable = pybamm.standard_spatial_vars.R_variable_p + f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + + # R-average + eta_r = pybamm.Integral(f_a_dist * eta_r, R_variable) + + # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) if eta_r.domain == []: eta_r = pybamm.FullBroadcast( @@ -559,8 +612,20 @@ def _get_standard_ocp_variables(self, ocp, dUdT): The variables dictionary including the open circuit potentials and related standard variables. """ + # If ocp depends on particle size R then must R-average to get standard + # output open circuit potential + if ocp.domain == [self.domain.lower() + " particle-size domain"]: + if self.domain == "Negative": + R_variable = pybamm.standard_spatial_vars.R_variable_n + f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + elif self.domain == "Positive": + R_variable = pybamm.standard_spatial_vars.R_variable_p + f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) - # Average, and broadcast if necessary + # R-average + ocp = pybamm.Integral(f_a_dist * ocp, R_variable) + + # X-average, and broadcast if necessary if ocp.domain == []: ocp_av = ocp ocp = pybamm.FullBroadcast( @@ -571,6 +636,20 @@ def _get_standard_ocp_variables(self, ocp, dUdT): ocp = pybamm.PrimaryBroadcast(ocp, self.domain_for_broadcast) else: ocp_av = pybamm.x_average(ocp) + + # If dUdT depends on particle size R then must R-average to get standard + # output entropic change + if dUdT.domain == [self.domain.lower() + " particle-size domain"]: + if self.domain == "Negative": + R_variable = pybamm.standard_spatial_vars.R_variable_n + f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + elif self.domain == "Positive": + R_variable = pybamm.standard_spatial_vars.R_variable_p + f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + + # R-average + dUdT = pybamm.Integral(f_a_dist * dUdT, R_variable) + dUdT_av = pybamm.x_average(dUdT) if self.domain == "Negative": diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index 1c25146e78..980f98d0b8 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -58,9 +58,18 @@ def get_coupled_variables(self, variables): if self.domain + " electrode surface potential difference" not in variables: variables = self._get_delta_phi(variables) delta_phi = variables[self.domain + " electrode surface potential difference"] - # If delta_phi was broadcast, take only the orphan + # If delta_phi was broadcast, take only the orphan. if isinstance(delta_phi, pybamm.Broadcast): delta_phi = delta_phi.orphans[0] + # For "particle-size distribution" models, delta_phi must then be + # broadcast to "particle-size domain" + if ( + self.reaction == "lithium-ion main" + and self.options["particle-size distribution"] == True + ): + delta_phi = pybamm.PrimaryBroadcast( + delta_phi, [self.domain.lower() + " particle-size domain"] + ) # Get exchange-current density j0 = self._get_exchange_current_density(variables) @@ -109,9 +118,21 @@ def get_coupled_variables(self, variables): T = variables[self.domain + " electrode temperature"] # Update j, except in the "distributed SEI resistance" model, where j will be - # found by solving an algebraic equation + # found by solving an algebraic equation. # (In the "distributed SEI resistance" model, we have already defined j) - j = self._get_kinetics(j0, ne, eta_r, T) + # Calculate j differently for "particle-size distribution" + if ( + self.reaction == "lithium-ion main" + and self.options["particle-size distribution"] == True + ): + j, j_distribution = self._get_PSD_current_densities(j0, ne, eta_r, T) + variables.update( + self._get_standard_PSD_interfacial_current_variables(j_distribution) + ) + + else: + j = self._get_kinetics(j0, ne, eta_r, T) + variables.update(self._get_standard_interfacial_current_variables(j)) variables.update( @@ -236,3 +257,62 @@ def _get_j_diffusion_limited_first_order(self, variables): since the reaction is not diffusion-limited """ return pybamm.Scalar(0) + + def _get_PSD_current_densities(self, j0, ne, eta_r, T): + """ + Calculates current densities that depend on particle size for the + particle-size distribution models. + """ + # T must have same domains as j0, eta_r, so reverse any broadcast to + # "electrode" then broadcast onto "particle-size domain" + if isinstance(T, pybamm.Broadcast): + T = T.orphans[0] + T = pybamm.PrimaryBroadcast( + T, [self.domain.lower() + " particle-size domain"] + ) + + # current density that depends on particle size + j_distribution = self._get_kinetics(j0, ne, eta_r, T) + if self.domain == "Negative": + R_variable = pybamm.standard_spatial_vars.R_variable_n + f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + elif self.domain == "Positive": + R_variable = pybamm.standard_spatial_vars.R_variable_p + f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + + # R-average + j = pybamm.Integral(f_a_dist * j_distribution, R_variable)#pybamm.PrimaryBroadcast(1, ["current collector"])# + return j, j_distribution + + def _get_standard_PSD_interfacial_current_variables(self, j_distribution): + """ + Blah blah + """ + # X-average and broadcast if necessary + if self.domain.lower() + " electrode" in j_distribution.auxiliary_domains: + + if self.domain == "Negative": + l = self.param.l_n + x = pybamm.standard_spatial_vars.x_n + elif self.domain == "Positive": + l = self.param.l_p + x = pybamm.standard_spatial_vars.x_p + # x-average + j_xav_distribution = pybamm.Integral(j_distribution, x) / l + else: + j_xav_distribution = j_distribution + j_distribution = pybamm.SecondaryBroadcast( + j_xav_distribution, [self.domain.lower() + " electrode"] + ) + + variables = { + self.domain + + " electrode interfacial current density" + + " distribution": j_distribution, + "X-averaged " + + self.domain.lower() + + " electrode interfacial current density" + + " distribution": j_xav_distribution, + } + + return variables diff --git a/pybamm/models/submodels/particle/__init__.py b/pybamm/models/submodels/particle/__init__.py index 374c59674c..a5a9b8f1a8 100644 --- a/pybamm/models/submodels/particle/__init__.py +++ b/pybamm/models/submodels/particle/__init__.py @@ -1,5 +1,6 @@ from .base_particle import BaseParticle from .fickian_many_particles import FickianManyParticles from .fickian_single_particle import FickianSingleParticle +from .fickian_single_particle_size_distribution import FickianSinglePSD from .fast_many_particles import FastManyParticles from .fast_single_particle import FastSingleParticle diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py new file mode 100644 index 0000000000..844e71235c --- /dev/null +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -0,0 +1,264 @@ +# +# Class for a single particle with Fickian diffusion +# +import pybamm + +from .base_particle import BaseParticle + + +class FickianSinglePSD(BaseParticle): + """Base class for molar conservation in a single x-averaged particle which employs + Fick's law. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + domain : str + The domain of the model either 'Negative' or 'Positive' + + + **Extends:** :class:`pybamm.particle.BaseParticle` + """ + + def __init__(self, param, domain): + super().__init__(param, domain) + + def get_fundamental_variables(self): + if self.domain == "Negative": + # distribution variables + c_s_xav_distribution = pybamm.Variable( + "X-averaged negative particle concentration distribution", + domain="negative particle", + auxiliary_domains={ + "secondary": "negative particle-size domain", + "tertiary": "current collector", + }, + bounds=(0, 1), + ) + j_xav_distribution = pybamm.Variable( + "X-averaged negative electrode interfacial current density", + domain="negative particle-size domain", + auxiliary_domains={"secondary": "current collector",}, + ) + R_variable = pybamm.standard_spatial_vars.R_variable_n + + # Additional parameters for this model + # Dimensionless standard deviation + sd_a = self.param.sd_a_n + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_n(R_variable, 1, sd_a) + + elif self.domain == "Positive": + # distribution variables + c_s_xav_distribution = pybamm.Variable( + "X-averaged positive particle concentration distribution", + domain="positive particle", + auxiliary_domains={ + "secondary": "positive particle-size domain", + "tertiary": "current collector", + }, + bounds=(0, 1), + ) + j_xav_distribution = pybamm.Variable( + "X-averaged positive electrode interfacial current density", + domain="positive particle-size domain", + auxiliary_domains={"secondary": "current collector"}, + ) + R_variable = pybamm.standard_spatial_vars.R_variable_p + + # Additional parameters for this model + # Dimensionless standard deviation + sd_a = self.param.sd_a_p + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_p(R_variable, 1, sd_a) + + # R-averaged variables + c_s_xav = pybamm.Integral(f_a_dist * c_s_xav_distribution, R_variable) + c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) + + c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + variables = self._get_standard_concentration_variables(c_s, c_s_xav) + variables.update( + { + self.domain + " particle size": R_variable, + "X-averaged " + + self.domain.lower() + + " particle concentration distribution": c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration" + + " distribution": c_s_surf_xav_distribution, + self.domain + + " particle surface concentration" + + " distribution": c_s_surf_distribution, + self.domain + + " area-weighted particle-size" + + " distribution": f_a_dist, + } + ) + return variables + + def get_coupled_variables(self, variables): + c_s_xav_distribution = variables[ + "X-averaged " + self.domain.lower() + " particle concentration distribution" + ] + R_variable = variables[self.domain + " particle size"] + + # broadcast to particle-size domain + T_k_xav = pybamm.PrimaryBroadcast( + variables["X-averaged " + self.domain.lower() + " electrode temperature"], + [self.domain.lower() + " particle-size domain"], + ) + + # broadcast again into particle + T_k_xav = pybamm.PrimaryBroadcast(T_k_xav, [self.domain.lower() + " particle"],) + + if self.domain == "Negative": + N_s_xav_distribution = -self.param.D_n( + c_s_xav_distribution, T_k_xav + ) * pybamm.grad(c_s_xav_distribution) + f_a_dist = variables["Negative area-weighted particle-size distribution"] + N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_variable) + elif self.domain == "Positive": + N_s_xav_distribution = -self.param.D_p( + c_s_xav_distribution, T_k_xav + ) * pybamm.grad(c_s_xav_distribution) + f_a_dist = variables["Positive area-weighted particle-size distribution"] + N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_variable) + + N_s = pybamm.SecondaryBroadcast(N_s_xav, [self._domain.lower() + " electrode"]) + + variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) + variables.update( + { + "X-averaged " + + self.domain.lower() + + " particle flux distribution": N_s_xav_distribution, + } + ) + return variables + + def set_rhs(self, variables): + c_s_xav_distribution = variables[ + "X-averaged " + self.domain.lower() + " particle concentration distribution" + ] + + N_s_xav_distribution = variables[ + "X-averaged " + self.domain.lower() + " particle flux distribution" + ] + + R_variable = variables[self.domain + " particle size"] + if self.domain == "Negative": + self.rhs = { + c_s_xav_distribution: -(1 / self.param.C_n) + * pybamm.div(N_s_xav_distribution) + / R_variable ** 2 + } + elif self.domain == "Positive": + self.rhs = { + c_s_xav_distribution: -(1 / self.param.C_p) + * pybamm.div(N_s_xav_distribution) + / R_variable ** 2 + } + + def set_boundary_conditions(self, variables): + c_s_xav_distribution = variables[ + "X-averaged " + self.domain.lower() + " particle concentration distribution" + ] + + c_s_surf_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution" + ] + + j_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " electrode interfacial current density distribution" + ] + + T_k_xav = variables[ + "X-averaged " + self.domain.lower() + " electrode temperature" + ] + T_k_xav = pybamm.PrimaryBroadcast( + T_k_xav, [self.domain.lower() + " particle-size domain"] + ) + + R_variable = variables[self.domain + " particle size"] + + if self.domain == "Negative": + rbc = ( + -self.param.C_n + * R_variable + * j_xav_distribution + / self.param.a_n + / self.param.D_n(c_s_surf_xav_distribution, T_k_xav) + ) + + elif self.domain == "Positive": + rbc = ( + -self.param.C_p + * R_variable + * j_xav_distribution + / self.param.a_p + / self.param.gamma_p + / self.param.D_p(c_s_surf_xav_distribution, T_k_xav) + ) + + self.boundary_conditions = { + c_s_xav_distribution: { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (rbc, "Neumann"), + } + } + + def set_initial_conditions(self, variables): + """ + For single particle-size distribution models, initial conditions can't + depend on x so we arbitrarily set the initial values of the single + particles to be given by the values at x=0 in the negative electrode + and x=1 in the positive electrode. Typically, supplied initial + conditions are uniform x. + """ + c_s_xav_distribution = variables[ + "X-averaged " + self.domain.lower() + " particle concentration distribution" + ] + + if self.domain == "Negative": + c_init = self.param.c_n_init(0) + + elif self.domain == "Positive": + c_init = self.param.c_p_init(1) + + self.initial_conditions = {c_s_xav_distribution: c_init} + + def set_events(self, variables): + c_s_surf_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution" + ] + tol = 1e-4 + + self.events.append( + pybamm.Event( + "Minumum " + self.domain.lower() + " particle surface concentration", + pybamm.min(c_s_surf_xav_distribution) - tol, + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Maximum " + self.domain.lower() + " particle surface concentration", + (1 - tol) - pybamm.max(c_s_surf_xav_distribution), + pybamm.EventType.TERMINATION, + ) + ) diff --git a/pybamm/parameters/geometric_parameters.py b/pybamm/parameters/geometric_parameters.py index ee4554b258..06226f82e0 100644 --- a/pybamm/parameters/geometric_parameters.py +++ b/pybamm/parameters/geometric_parameters.py @@ -44,6 +44,12 @@ b_s_n = pybamm.Parameter("Negative electrode Bruggeman coefficient (electrode)") b_s_s = pybamm.Parameter("Separator Bruggeman coefficient (electrode)") b_s_p = pybamm.Parameter("Positive electrode Bruggeman coefficient (electrode)") +sd_a_n_dim = pybamm.Parameter( + "Negative area-weighted particle-size standard deviation [m]" +) +sd_a_p_dim = pybamm.Parameter( + "Positive area-weighted particle-size standard deviation [m]" +) # -------------------------------------------------------------------------------------- "Dimensionless Parameters" @@ -70,3 +76,7 @@ l_tab_p = L_tab_p / L_z centre_y_tab_p = Centre_y_tab_p / L_z centre_z_tab_p = Centre_z_tab_p / L_z + +# Microscale geometry +sd_a_n = sd_a_n_dim / R_n +sd_a_p = sd_a_p_dim / R_p diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index aea8467024..9eeadbd06c 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -89,6 +89,8 @@ b_s_n = pybamm.geometric_parameters.b_s_n b_s_s = pybamm.geometric_parameters.b_s_s b_s_p = pybamm.geometric_parameters.b_s_p +sd_a_n_dim = pybamm.geometric_parameters.sd_a_n_dim +sd_a_p_dim = pybamm.geometric_parameters.sd_a_p_dim # Electrochemical reactions ne_n = pybamm.Parameter("Negative electrode electrons in reaction") @@ -273,6 +275,31 @@ def U_p_dimensional(sto, T): j0_n_ref_dimensional = j0_n_dimensional(c_e_typ, c_n_max / 2, T_ref) * 2 j0_p_ref_dimensional = j0_p_dimensional(c_e_typ, c_p_max / 2, T_ref) * 2 +# Area-weighted particle-size distributions +def f_a_dist_n_dimensional(R, R_av_a, sd_a): + "Dimensional negative electrode particle-size distribution (area-weighted)" + inputs = { + "Negative particle-size variable [m]": R, + "Negative area-weighted mean particle size [m]": R_av_a, + "Negative area-weighted particle-size standard deviation [m]": sd_a, + } + return pybamm.FunctionParameter( + "Negative area-weighted particle-size distribution [m]", inputs, + ) + + +def f_a_dist_p_dimensional(R, R_av_a, sd_a): + "Dimensional positive electrode particle-size distribution (area-weighted)" + inputs = { + "Positive particle-size variable [m]": R, + "Positive area-weighted mean particle size [m]": R_av_a, + "Positive area-weighted particle-size standard deviation [m]": sd_a, + } + return pybamm.FunctionParameter( + "Positive area-weighted particle-size distribution [m]", inputs, + ) + + # ------------------------------------------------------------------------------------- "3. Scales" # concentration @@ -367,6 +394,8 @@ def U_p_dimensional(sto, T): epsilon_inactive_p = 1 - epsilon_p - epsilon_s_p a_n = a_n_dim * R_n a_p = a_p_dim * R_p +sd_a_n = sd_a_n_dim / R_n +sd_a_p = sd_a_p_dim / R_p # Electrode Properties sigma_cn = sigma_cn_dimensional * potential_scale / i_typ / L_x @@ -592,6 +621,23 @@ def dUdT_p(c_s_p): return dUdT_p_dimensional(sto) * Delta_T / potential_scale +# Area-weighted particle-size distributions +def f_a_dist_n(R, R_av_a, sd_a): + "Dimensionless negative electrode particle-size distribution (area-weighted)" + R_dim = R * R_n + R_av_a_dim = R_av_a * R_n + sd_a_dim = sd_a * R_n + return f_a_dist_n_dimensional(R_dim, R_av_a_dim, sd_a_dim) * R_n + + +def f_a_dist_p(R, R_av_a, sd_a): + "Dimensionless positive electrode particle-size distribution (area-weighted)" + R_dim = R * R_p + R_av_a_dim = R_av_a * R_p + sd_a_dim = sd_a * R_p + return f_a_dist_p_dimensional(R_dim, R_av_a_dim, sd_a_dim) * R_p + + # -------------------------------------------------------------------------------------- # 6. Input current and voltage From ad2b172fc55ef9b45396ea0ab10c523fe0dc41a4 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Mon, 13 Jul 2020 16:08:21 +0100 Subject: [PATCH 08/67] tidied up PSDModel --- .../full_battery_models/lithium_ion/psd_model.py | 12 ------------ .../electrode/ohm/leading_size_distribution_ohm.py | 7 +------ 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/psd_model.py b/pybamm/models/full_battery_models/lithium_ion/psd_model.py index 3d5ec3fc9e..bddcc025e6 100644 --- a/pybamm/models/full_battery_models/lithium_ion/psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/psd_model.py @@ -179,18 +179,6 @@ def default_parameter_values(self): # lognormal area-weighted particle-size distribution - def lognormal_distribution(R, R_av, sd): - import numpy as np - - # inputs are particle radius R, the mean R_av, and standard deviation sd - # inputs can be dimensional or dimensionless - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R + 0.01) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / (R + 0.01) - ) def lognormal_distribution(R, R_av, sd): import numpy as np diff --git a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py index 5b63f2db87..6879289738 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py @@ -95,12 +95,7 @@ def set_algebraic(self, variables): + self.domain.lower() + " electrode surface potential difference" ] -# i_cell = self.param.current_with_time -# if self.domain == "Negative": -# j_tot_av = i_cell/self.param.l_n -# elif self.domain == "Positive": -# j_tot_av = -i_cell/self.param.l_p - self.algebraic[delta_phi_av] = sum_j_av - j_tot_av#delta_phi_av - 1# + self.algebraic[delta_phi_av] = sum_j_av - j_tot_av def set_initial_conditions(self, variables): From a2b7df83c90fa9503facd4406413b22351db78b9 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Mon, 13 Jul 2020 19:52:51 +0100 Subject: [PATCH 09/67] quick plotting for 2D vars of particle size --- .../lithium_ion/basic_psd_model.py | 3 ++ pybamm/plotting/quick_plot.py | 32 ++++++++++++++++--- pybamm/solvers/processed_variable.py | 26 +++++++++++++-- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py index 09ee87d77e..ed9748d656 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py @@ -533,3 +533,6 @@ def default_spatial_methods(self): } ) return default_spatials + + def new_copy(self, build=False): + return pybamm.BaseModel.new_copy(self) \ No newline at end of file diff --git a/pybamm/plotting/quick_plot.py b/pybamm/plotting/quick_plot.py index 5b1d224a18..b9cf511b62 100644 --- a/pybamm/plotting/quick_plot.py +++ b/pybamm/plotting/quick_plot.py @@ -252,6 +252,8 @@ def set_output_variables(self, output_variables, solutions): self.second_dimensional_spatial_variable = {} self.is_x_r = {} self.is_y_z = {} + self.is_x_R = {} + self.is_R_r = {} # Calculate subplot positions based on number of variables supplied self.subplot_positions = {} @@ -337,14 +339,34 @@ def set_output_variables(self, output_variables, solutions): if first_spatial_var_name == "r" and second_spatial_var_name == "x": self.is_x_r[variable_tuple] = True self.is_y_z[variable_tuple] = False + self.is_x_R[variable_tuple] = False + self.is_R_r[variable_tuple] = False elif ( first_spatial_var_name == "y" and second_spatial_var_name == "z" ): self.is_x_r[variable_tuple] = False self.is_y_z[variable_tuple] = True + self.is_x_R[variable_tuple] = False + self.is_R_r[variable_tuple] = False + elif ( + first_spatial_var_name == "R" and second_spatial_var_name == "x" + ): + self.is_x_r[variable_tuple] = False + self.is_y_z[variable_tuple] = False + self.is_x_R[variable_tuple] = True + self.is_R_r[variable_tuple] = False + elif ( + first_spatial_var_name == "r" and second_spatial_var_name == "R" + ): + self.is_x_r[variable_tuple] = False + self.is_y_z[variable_tuple] = False + self.is_x_R[variable_tuple] = False + self.is_R_r[variable_tuple] = True else: self.is_x_r[variable_tuple] = False self.is_y_z[variable_tuple] = False + self.is_x_R[variable_tuple] = False + self.is_R_r[variable_tuple] = False # Store variables and subplot position self.variables[variable_tuple] = variables @@ -388,8 +410,8 @@ def reset_axis(self): x_min = self.first_dimensional_spatial_variable[key][0] x_max = self.first_dimensional_spatial_variable[key][-1] elif variable_lists[0][0].dimensions == 2: - # different order based on whether the domains are x-r, x-z or y-z - if self.is_x_r[key] is True: + # different order based on whether the domains are x-r, x-z or y-z, etc + if (self.is_x_r[key] or self.is_x_R[key] or self.is_R_r[key]) is True: x_min = self.second_dimensional_spatial_variable[key][0] x_max = self.second_dimensional_spatial_variable[key][-1] y_min = self.first_dimensional_spatial_variable[key][0] @@ -554,8 +576,8 @@ def plot(self, t): spatial_vars = self.spatial_variable_dict[key] # there can only be one entry in the variable list variable = variable_lists[0][0] - # different order based on whether the domains are x-r, x-z or y-z - if self.is_x_r[key] is True: + # different order based on whether the domains are x-r, x-z or y-z, etc + if (self.is_x_r[key] or self.is_x_R[key] or self.is_R_r[key]) is True: x_name = list(spatial_vars.keys())[1][0] y_name = list(spatial_vars.keys())[0][0] x = self.second_dimensional_spatial_variable[key] @@ -709,7 +731,7 @@ def slider_update(self, t): # there can only be one entry in the variable list variable = self.variables[key][0][0] vmin, vmax = self.variable_limits[key] - if self.is_x_r[key] is True: + if (self.is_x_r[key] or self.is_x_R[key] or self.is_R_r[key]) is True: x = self.second_dimensional_spatial_variable[key] y = self.first_dimensional_spatial_variable[key] var = variable(time_in_seconds, **spatial_vars, warn=False) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 5eee3b778e..28281bea29 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -263,7 +263,7 @@ def interp_fun(t, z): def initialise_2D(self): """ - Initialise a 2D object that depends on x and r, or x and z. + Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. """ first_dim_nodes = self.mesh.nodes first_dim_edges = self.mesh.edges @@ -348,7 +348,7 @@ def initialise_2D(self): axis=1, ) - # Process r-x or x-z + # Process r-x, x-z, r-R, or R-x if self.domain[0] in [ "negative particle", "positive particle", @@ -369,6 +369,28 @@ def initialise_2D(self): self.second_dimension = "z" self.x_sol = first_dim_pts self.z_sol = second_dim_pts + elif self.domain[0] in [ + "negative particle", + "positive particle", + ] and self.auxiliary_domains["secondary"][0] in [ + "negative particle-size domain", + "positive particle-size domain", + ]: + self.first_dimension = "r" + self.second_dimension = "R" + self.r_sol = first_dim_pts + self.R_sol = second_dim_pts + elif self.domain[0] in [ + "negative particle-size domain", + "positive particle-size domain", + ] and self.auxiliary_domains["secondary"][0] in [ + "negative electrode", + "positive electrode", + ]: + self.first_dimension = "R" + self.second_dimension = "x" + self.R_sol = first_dim_pts + self.x_sol = second_dim_pts else: raise pybamm.DomainError( "Cannot process 3D object with domain '{}' " From 534b60bcd0363912e80fca9ce19a4f4e7f882a4c Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 14 Jul 2020 16:20:36 +0100 Subject: [PATCH 10/67] tidied up PSD output variables --- .../full_battery_models/base_battery_model.py | 9 +- .../lithium_ion/base_lithium_ion_model.py | 6 + .../lithium_ion/psd_model.py | 80 +++++------ .../submodels/interface/base_interface.py | 114 +++++++++++++--- .../interface/kinetics/base_kinetics.py | 64 +-------- ...ckian_single_particle_size_distribution.py | 127 ++++++++++-------- .../standard_parameters_lithium_ion.py | 22 ++- 7 files changed, 229 insertions(+), 193 deletions(-) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 8319ffedf1..dfd0184456 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -41,6 +41,9 @@ class BaseBatteryModel(pybamm.BaseModel): * "particle" : str, optional Sets the submodel to use to describe behaviour within the particle. Can be "Fickian diffusion" (default) or "fast diffusion". + * "particle-size distribution" : bool, optional + Whether to include a distribution of particle sizes or a single size for + each electrode. Can be True or False (default). * "thermal" : str, optional Sets the thermal model to use. Can be "isothermal" (default), "lumped", "x-lumped", or "x-full". @@ -339,7 +342,11 @@ def options(self, extra_options): raise pybamm.OptionError( "particle model '{}' not recognised".format(options["particle"]) ) - + if options["particle-size distribution"] not in [True, False]: + raise pybamm.OptionError( + "Particle-size distribution must be True or False, option " + "'{}' not recognised".format(options["particle-size distribution"]) + ) if options["thermal"] == "x-lumped" and options["dimensionality"] == 1: warnings.warn( "1+1D Thermal models are only valid if both tabs are " diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index e32fb95329..9e44cf9d2d 100644 --- a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -27,6 +27,8 @@ def __init__(self, options=None, name="Unnamed lithium-ion model"): "positive electrode": self.param.L_x, "negative particle": self.param.R_n, "positive particle": self.param.R_p, + "negative particle-size domain": self.param.R_n, + "positive particle-size domain": self.param.R_p, "current collector y": self.param.L_y, "current collector z": self.param.L_z, } @@ -44,6 +46,10 @@ def set_standard_output_variables(self): "r_n [m]": var.r_n * param.R_n, "r_p": var.r_p, "r_p [m]": var.r_p * param.R_p, + "Negative particle size": var.R_variable_n, + "Negative particle size [m]": var.R_variable_n * param.R_n, + "Positive particle size": var.R_variable_p, + "Positive particle size [m]": var.R_variable_p * param.R_p, } ) diff --git a/pybamm/models/full_battery_models/lithium_ion/psd_model.py b/pybamm/models/full_battery_models/lithium_ion/psd_model.py index bddcc025e6..dbdb0278c0 100644 --- a/pybamm/models/full_battery_models/lithium_ion/psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/psd_model.py @@ -22,8 +22,9 @@ class PSDModel(BaseModel): References ---------- - .. [1] TL Kirk, J Evans, CP Please and SJ Chapman. “Modelling electrode heterogeneity - in lithium-ion batteries: unimodal and bimodal particle-size distributions”. + .. [1] TL Kirk, J Evans, CP Please and SJ Chapman. “Modelling electrode + heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size + distributions”. In: arXiv preprint arXiv:2006.12208 (2020). **Extends:** :class:`pybamm.lithium_ion.BaseModel` @@ -35,16 +36,6 @@ def __init__( super().__init__(options, name) self.options["particle-size distribution"] = True - # Set length scales for additional domains (particle-size domains) - self.length_scales.update( - { - "negative particle-size domain": self.param.R_n, - "positive particle-size domain": self.param.R_p, - } - ) - # Update standard output variables - self.set_standard_output_variables() - # Set submodels self.set_external_circuit_submodel() self.set_porosity_submodel() @@ -140,8 +131,8 @@ def set_electrolyte_submodel(self): self.submodels[ "electrolyte diffusion" ] = pybamm.electrolyte_diffusion.ConstantConcentration(self.param) - - def set_standard_output_variables(self): + """ + def set_standard_output_variables(self): super().set_standard_output_variables() # add particle-size variables @@ -156,29 +147,30 @@ def set_standard_output_variables(self): "Positive particle size [m]": var.R_variable_p * R_p, } ) - + """ #################### # Overwrite defaults #################### @property def default_parameter_values(self): # Default parameter values - # Lion parameters left as default parameter set for tests default_params = super().default_parameter_values + R_n_dim = default_params["Negative particle radius [m]"] + R_p_dim = default_params["Positive particle radius [m]"] # New parameter values # Area-weighted standard deviations sd_a_n = 0.5 sd_a_p = 0.3 - sd_a_n_dim = sd_a_n * default_params["Negative particle radius [m]"] - sd_a_p_dim = sd_a_p * default_params["Positive particle radius [m]"] + sd_a_n_dim = sd_a_n * R_n_dim + sd_a_p_dim = sd_a_p * R_p_dim - # Max radius in the particle-size distribution (dimensionless) + # Max radius in the particle-size distribution (dimensionless). + # Either 5 s.d.'s above the mean or the value 2, whichever is larger R_n_max = max(2, 1 + sd_a_n * 5) R_p_max = max(2, 1 + sd_a_p * 5) # lognormal area-weighted particle-size distribution - def lognormal_distribution(R, R_av, sd): import numpy as np @@ -192,41 +184,29 @@ def lognormal_distribution(R, R_av, sd): / (R) ) - default_params.update( - {"Negative area-weighted particle-size standard deviation": sd_a_n}, - check_already_exists=False, - ) - default_params.update( - {"Negative area-weighted particle-size standard deviation [m]": sd_a_n_dim}, - check_already_exists=False, - ) - default_params.update( - {"Positive area-weighted particle-size standard deviation": sd_a_p}, - check_already_exists=False, - ) - default_params.update( - {"Positive area-weighted particle-size standard deviation [m]": sd_a_p_dim}, - check_already_exists=False, - ) - default_params.update( - {"Negative maximum particle radius": R_n_max}, check_already_exists=False - ) - default_params.update( - {"Positive maximum particle radius": R_p_max}, check_already_exists=False - ) - default_params.update( - { - "Negative area-weighted particle-size distribution [m]": lognormal_distribution - }, - check_already_exists=False, - ) + def f_a_dist_n_dim(R): + return lognormal_distribution(R, R_n_dim, sd_a_n_dim) + + def f_a_dist_p_dim(R): + return lognormal_distribution(R, R_p_dim, sd_a_p_dim) + default_params.update( { - "Positive area-weighted particle-size distribution [m]": lognormal_distribution + "Negative area-weighted particle-size standard deviation": sd_a_n, + "Negative area-weighted particle-size " + + "standard deviation [m]": sd_a_n_dim, + "Positive area-weighted particle-size standard deviation": sd_a_p, + "Positive area-weighted particle-size " + + "standard deviation [m]": sd_a_p_dim, + "Negative maximum particle radius": R_n_max, + "Positive maximum particle radius": R_p_max, + "Negative area-weighted " + + "particle-size distribution [m]": f_a_dist_n_dim, + "Positive area-weighted " + + "particle-size distribution [m]": f_a_dist_p_dim, }, check_already_exists=False, ) - return default_params @property diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index ef3be3bc6a..6f9b816e2f 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -60,7 +60,7 @@ def _get_exchange_current_density(self, variables): if self.reaction == "lithium-ion main": # For "particle-size distribution", take distribution version # of c_s_surf that depends on particle size. - if self.options["particle-size distribution"] == True: + if self.options["particle-size distribution"]: c_s_surf = variables[ self.domain + " particle surface concentration distribution" ] @@ -78,7 +78,7 @@ def _get_exchange_current_density(self, variables): T = T.orphans[0] # For "particle-size distribution", broadcast c_e, T onto particle-size # domain - if self.options["particle-size distribution"] == True: + if self.options["particle-size distribution"]: c_e = pybamm.PrimaryBroadcast( c_e, [self.domain.lower() + " particle-size domain"] ) @@ -140,7 +140,7 @@ def _get_open_circuit_potential(self, variables): T = variables[self.domain + " electrode temperature"] # For "particle-size distribution" models, take distribution version # of c_s_surf that depends on particle size. - if self.options["particle-size distribution"] == True: + if self.options["particle-size distribution"]: c_s_surf = variables[ self.domain + " particle surface concentration distribution" ] @@ -148,13 +148,15 @@ def _get_open_circuit_potential(self, variables): c_s_surf = variables[self.domain + " particle surface concentration"] # If variable was broadcast, take only the orphan - if (isinstance(c_s_surf, pybamm.Broadcast) and isinstance(T, pybamm.Broadcast) + if ( + isinstance(c_s_surf, pybamm.Broadcast) + and isinstance(T, pybamm.Broadcast) ): c_s_surf = c_s_surf.orphans[0] T = T.orphans[0] - # For "particle-size distribution" models, then broadcast T onto particle-size - # domain - if self.options["particle-size distribution"] == True: + # For "particle-size distribution" models, then broadcast T + # onto particle-size domain + if self.options["particle-size distribution"]: T = pybamm.PrimaryBroadcast( T, [self.domain.lower() + " particle-size domain"] ) @@ -400,10 +402,10 @@ def _get_standard_exchange_current_variables(self, j0): if j0.domain == [self.domain.lower() + " particle-size domain"]: if self.domain == "Negative": R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + f_a_dist = self.param.f_a_dist_n(R_variable) elif self.domain == "Positive": R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + f_a_dist = self.param.f_a_dist_p(R_variable) # R-average j0 = pybamm.Integral(f_a_dist * j0, R_variable) @@ -494,10 +496,10 @@ def _get_standard_overpotential_variables(self, eta_r): if eta_r.domain == [self.domain.lower() + " particle-size domain"]: if self.domain == "Negative": R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + f_a_dist = self.param.f_a_dist_n(R_variable) elif self.domain == "Positive": R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + f_a_dist = self.param.f_a_dist_p(R_variable) # R-average eta_r = pybamm.Integral(f_a_dist * eta_r, R_variable) @@ -617,10 +619,10 @@ def _get_standard_ocp_variables(self, ocp, dUdT): if ocp.domain == [self.domain.lower() + " particle-size domain"]: if self.domain == "Negative": R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + f_a_dist = self.param.f_a_dist_n(R_variable) elif self.domain == "Positive": R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + f_a_dist = self.param.f_a_dist_p(R_variable) # R-average ocp = pybamm.Integral(f_a_dist * ocp, R_variable) @@ -642,10 +644,10 @@ def _get_standard_ocp_variables(self, ocp, dUdT): if dUdT.domain == [self.domain.lower() + " particle-size domain"]: if self.domain == "Negative": R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) + f_a_dist = self.param.f_a_dist_n(R_variable) elif self.domain == "Positive": R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) + f_a_dist = self.param.f_a_dist_p(R_variable) # R-average dUdT = pybamm.Integral(f_a_dist * dUdT, R_variable) @@ -690,3 +692,85 @@ def _get_standard_ocp_variables(self, ocp, dUdT): ) return variables + + def _get_PSD_current_densities(self, j0, ne, eta_r, T): + """ + Calculates current density (j_distribution) that depends on + particle size for "particle-size distribution" models, and + the standard R-averaged current density (j) + """ + # T must have same domains as j0, eta_r, so reverse any broadcast to + # "electrode" then broadcast onto "particle-size domain" + if isinstance(T, pybamm.Broadcast): + T = T.orphans[0] + T = pybamm.PrimaryBroadcast( + T, [self.domain.lower() + " particle-size domain"] + ) + + # current density that depends on particle size R + j_distribution = self._get_kinetics(j0, ne, eta_r, T) + if self.domain == "Negative": + R_variable = pybamm.standard_spatial_vars.R_variable_n + f_a_dist = self.param.f_a_dist_n(R_variable) + elif self.domain == "Positive": + R_variable = pybamm.standard_spatial_vars.R_variable_p + f_a_dist = self.param.f_a_dist_p(R_variable) + + # R-average + j = pybamm.Integral(f_a_dist * j_distribution, R_variable) + return j, j_distribution + + def _get_standard_PSD_interfacial_current_variables(self, j_distribution): + """ + Interfacial current density variables that depend on particle size R, + relevant if "particle-size distribution" option is True. + """ + # X-average and broadcast if necessary + if self.domain.lower() + " electrode" in j_distribution.auxiliary_domains: + + if self.domain == "Negative": + l = self.param.l_n + x = pybamm.standard_spatial_vars.x_n + elif self.domain == "Positive": + l = self.param.l_p + x = pybamm.standard_spatial_vars.x_p + # x-average + j_xav_distribution = pybamm.Integral(j_distribution, x) / l + else: + j_xav_distribution = j_distribution + j_distribution = pybamm.SecondaryBroadcast( + j_xav_distribution, [self.domain.lower() + " electrode"] + ) + + #j scale + i_typ = self.param.i_typ + L_x = self.param.L_x + if self.domain == "Negative": + j_scale = i_typ / (self.param.a_n_dim * L_x) + elif self.domain == "Positive": + j_scale = i_typ / (self.param.a_p_dim * L_x) + + variables = { + self.domain + + " electrode" + + self.reaction_name + + " interfacial current density distribution": j_distribution, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " interfacial current density distribution": j_xav_distribution, + self.domain + + " electrode" + + self.reaction_name + + " interfacial current density" + + " distribution [A.m-2]": j_scale * j_distribution, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " interfacial current density" + + " distribution [A.m-2]": j_scale * j_xav_distribution, + } + + return variables diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index 980f98d0b8..d0949c2254 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -65,7 +65,7 @@ def get_coupled_variables(self, variables): # broadcast to "particle-size domain" if ( self.reaction == "lithium-ion main" - and self.options["particle-size distribution"] == True + and self.options["particle-size distribution"] is True ): delta_phi = pybamm.PrimaryBroadcast( delta_phi, [self.domain.lower() + " particle-size domain"] @@ -120,11 +120,12 @@ def get_coupled_variables(self, variables): # Update j, except in the "distributed SEI resistance" model, where j will be # found by solving an algebraic equation. # (In the "distributed SEI resistance" model, we have already defined j) - # Calculate j differently for "particle-size distribution" if ( self.reaction == "lithium-ion main" - and self.options["particle-size distribution"] == True + and self.options["particle-size distribution"] is True ): + # For "particle-size distribution" models, additional steps (R-averaging) + # are necessary to calculate j j, j_distribution = self._get_PSD_current_densities(j0, ne, eta_r, T) variables.update( self._get_standard_PSD_interfacial_current_variables(j_distribution) @@ -258,61 +259,4 @@ def _get_j_diffusion_limited_first_order(self, variables): """ return pybamm.Scalar(0) - def _get_PSD_current_densities(self, j0, ne, eta_r, T): - """ - Calculates current densities that depend on particle size for the - particle-size distribution models. - """ - # T must have same domains as j0, eta_r, so reverse any broadcast to - # "electrode" then broadcast onto "particle-size domain" - if isinstance(T, pybamm.Broadcast): - T = T.orphans[0] - T = pybamm.PrimaryBroadcast( - T, [self.domain.lower() + " particle-size domain"] - ) - - # current density that depends on particle size - j_distribution = self._get_kinetics(j0, ne, eta_r, T) - if self.domain == "Negative": - R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable, 1, self.param.sd_a_n) - elif self.domain == "Positive": - R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable, 1, self.param.sd_a_p) - - # R-average - j = pybamm.Integral(f_a_dist * j_distribution, R_variable)#pybamm.PrimaryBroadcast(1, ["current collector"])# - return j, j_distribution - def _get_standard_PSD_interfacial_current_variables(self, j_distribution): - """ - Blah blah - """ - # X-average and broadcast if necessary - if self.domain.lower() + " electrode" in j_distribution.auxiliary_domains: - - if self.domain == "Negative": - l = self.param.l_n - x = pybamm.standard_spatial_vars.x_n - elif self.domain == "Positive": - l = self.param.l_p - x = pybamm.standard_spatial_vars.x_p - # x-average - j_xav_distribution = pybamm.Integral(j_distribution, x) / l - else: - j_xav_distribution = j_distribution - j_distribution = pybamm.SecondaryBroadcast( - j_xav_distribution, [self.domain.lower() + " electrode"] - ) - - variables = { - self.domain - + " electrode interfacial current density" - + " distribution": j_distribution, - "X-averaged " - + self.domain.lower() - + " electrode interfacial current density" - + " distribution": j_xav_distribution, - } - - return variables diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py index 844e71235c..326358e331 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -36,19 +36,11 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - j_xav_distribution = pybamm.Variable( - "X-averaged negative electrode interfacial current density", - domain="negative particle-size domain", - auxiliary_domains={"secondary": "current collector",}, - ) R_variable = pybamm.standard_spatial_vars.R_variable_n - - # Additional parameters for this model - # Dimensionless standard deviation - sd_a = self.param.sd_a_n + R_dim = self.param.R_n # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_variable, 1, sd_a) + f_a_dist = self.param.f_a_dist_n(R_variable) elif self.domain == "Positive": # distribution variables @@ -61,45 +53,31 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - j_xav_distribution = pybamm.Variable( - "X-averaged positive electrode interfacial current density", - domain="positive particle-size domain", - auxiliary_domains={"secondary": "current collector"}, - ) R_variable = pybamm.standard_spatial_vars.R_variable_p - - # Additional parameters for this model - # Dimensionless standard deviation - sd_a = self.param.sd_a_p + R_dim = self.param.R_p # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_variable, 1, sd_a) + f_a_dist = self.param.f_a_dist_p(R_variable) - # R-averaged variables + # Standard R-averaged variables c_s_xav = pybamm.Integral(f_a_dist * c_s_xav_distribution, R_variable) c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) + variables = self._get_standard_concentration_variables(c_s, c_s_xav) - c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) - c_s_surf_distribution = pybamm.SecondaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + # Standard distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables( + c_s_xav_distribution + ) ) - variables = self._get_standard_concentration_variables(c_s, c_s_xav) variables.update( { self.domain + " particle size": R_variable, - "X-averaged " - + self.domain.lower() - + " particle concentration distribution": c_s_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle surface concentration" - + " distribution": c_s_surf_xav_distribution, - self.domain - + " particle surface concentration" - + " distribution": c_s_surf_distribution, - self.domain - + " area-weighted particle-size" + self.domain + " particle size [m]": R_variable * R_dim, + self.domain + " area-weighted particle-size" + " distribution": f_a_dist, + self.domain + " area-weighted particle-size" + + " distribution [m]": f_a_dist / R_dim, } ) return variables @@ -109,32 +87,31 @@ def get_coupled_variables(self, variables): "X-averaged " + self.domain.lower() + " particle concentration distribution" ] R_variable = variables[self.domain + " particle size"] + f_a_dist = variables[self.domain + " area-weighted particle-size distribution"] - # broadcast to particle-size domain + # broadcast to particle-size domain then again into particle T_k_xav = pybamm.PrimaryBroadcast( variables["X-averaged " + self.domain.lower() + " electrode temperature"], [self.domain.lower() + " particle-size domain"], ) - - # broadcast again into particle T_k_xav = pybamm.PrimaryBroadcast(T_k_xav, [self.domain.lower() + " particle"],) if self.domain == "Negative": N_s_xav_distribution = -self.param.D_n( c_s_xav_distribution, T_k_xav - ) * pybamm.grad(c_s_xav_distribution) - f_a_dist = variables["Negative area-weighted particle-size distribution"] - N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_variable) + ) * pybamm.grad(c_s_xav_distribution) / R_variable elif self.domain == "Positive": N_s_xav_distribution = -self.param.D_p( c_s_xav_distribution, T_k_xav - ) * pybamm.grad(c_s_xav_distribution) - f_a_dist = variables["Positive area-weighted particle-size distribution"] - N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_variable) + ) * pybamm.grad(c_s_xav_distribution) / R_variable + # Standard R-averaged flux variables + N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_variable) N_s = pybamm.SecondaryBroadcast(N_s_xav, [self._domain.lower() + " electrode"]) - variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) + + # Standard distribution flux variables (R-dependent) + # (Cannot currently broadcast to "x" as it is a tertiary domain) variables.update( { "X-averaged " @@ -158,32 +135,33 @@ def set_rhs(self, variables): self.rhs = { c_s_xav_distribution: -(1 / self.param.C_n) * pybamm.div(N_s_xav_distribution) - / R_variable ** 2 + / R_variable } elif self.domain == "Positive": self.rhs = { c_s_xav_distribution: -(1 / self.param.C_p) * pybamm.div(N_s_xav_distribution) - / R_variable ** 2 + / R_variable } def set_boundary_conditions(self, variables): + # Extract variables c_s_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " particle concentration distribution" ] - c_s_surf_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " particle surface concentration distribution" ] - j_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " electrode interfacial current density distribution" ] + R_variable = variables[self.domain + " particle size"] + # Extract x-av T and broadcast to particle-size domain T_k_xav = variables[ "X-averaged " + self.domain.lower() + " electrode temperature" ] @@ -191,8 +169,7 @@ def set_boundary_conditions(self, variables): T_k_xav, [self.domain.lower() + " particle-size domain"] ) - R_variable = variables[self.domain + " particle size"] - + # Set surface Neumann boundary values if self.domain == "Negative": rbc = ( -self.param.C_n @@ -262,3 +239,47 @@ def set_events(self, variables): pybamm.EventType.TERMINATION, ) ) + + def _get_standard_concentration_distribution_variables(self, c_s_xav_distribution): + ''' + Forms concentration variables that depend on particle size R given the input + c_s_distribution. + ''' + # Currently not possible to broadcast from (r, R) to (r, R, x) since + # domain x for broadcast is in "tertiary" position. + + # Surface concentration distribution variables + c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + if self.domain == "Negative": + c_scale = self.param.c_n_max + elif self.domain == "Positive": + c_scale = self.param.c_p_max + + variables = { + "X-averaged " + + self.domain.lower() + + " particle concentration distribution": c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle concentration distribution " + + "[mol.m-3]": c_scale * c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration" + + " distribution": c_s_surf_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution " + + "[mol.m-3]": c_scale * c_s_surf_xav_distribution, + self.domain + + " particle surface concentration" + + " distribution": c_s_surf_distribution, + self.domain + + " particle surface concentration" + + " distribution [mol.m-3]": c_scale * c_s_surf_distribution, + } + return variables + \ No newline at end of file diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index 9eeadbd06c..ecba4aba8c 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -276,24 +276,22 @@ def U_p_dimensional(sto, T): j0_p_ref_dimensional = j0_p_dimensional(c_e_typ, c_p_max / 2, T_ref) * 2 # Area-weighted particle-size distributions -def f_a_dist_n_dimensional(R, R_av_a, sd_a): + + +def f_a_dist_n_dimensional(R): "Dimensional negative electrode particle-size distribution (area-weighted)" inputs = { "Negative particle-size variable [m]": R, - "Negative area-weighted mean particle size [m]": R_av_a, - "Negative area-weighted particle-size standard deviation [m]": sd_a, } return pybamm.FunctionParameter( "Negative area-weighted particle-size distribution [m]", inputs, ) -def f_a_dist_p_dimensional(R, R_av_a, sd_a): +def f_a_dist_p_dimensional(R): "Dimensional positive electrode particle-size distribution (area-weighted)" inputs = { "Positive particle-size variable [m]": R, - "Positive area-weighted mean particle size [m]": R_av_a, - "Positive area-weighted particle-size standard deviation [m]": sd_a, } return pybamm.FunctionParameter( "Positive area-weighted particle-size distribution [m]", inputs, @@ -622,20 +620,16 @@ def dUdT_p(c_s_p): # Area-weighted particle-size distributions -def f_a_dist_n(R, R_av_a, sd_a): +def f_a_dist_n(R): "Dimensionless negative electrode particle-size distribution (area-weighted)" R_dim = R * R_n - R_av_a_dim = R_av_a * R_n - sd_a_dim = sd_a * R_n - return f_a_dist_n_dimensional(R_dim, R_av_a_dim, sd_a_dim) * R_n + return f_a_dist_n_dimensional(R_dim) * R_n -def f_a_dist_p(R, R_av_a, sd_a): +def f_a_dist_p(R): "Dimensionless positive electrode particle-size distribution (area-weighted)" R_dim = R * R_p - R_av_a_dim = R_av_a * R_p - sd_a_dim = sd_a * R_p - return f_a_dist_p_dimensional(R_dim, R_av_a_dim, sd_a_dim) * R_p + return f_a_dist_p_dimensional(R_dim) * R_p # -------------------------------------------------------------------------------------- From 8487ef9afcffff2bd77ab45d10667e2a0c4ccfe0 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 14 Jul 2020 17:44:41 +0100 Subject: [PATCH 11/67] added fast diffusion PSD submodels --- pybamm/models/submodels/particle/__init__.py | 1 + .../submodels/particle/base_particle.py | 62 ++++++ .../fast_single_particle_size_distribution.py | 179 ++++++++++++++++++ ...ckian_single_particle_size_distribution.py | 51 +---- 4 files changed, 247 insertions(+), 46 deletions(-) create mode 100644 pybamm/models/submodels/particle/fast_single_particle_size_distribution.py diff --git a/pybamm/models/submodels/particle/__init__.py b/pybamm/models/submodels/particle/__init__.py index a5a9b8f1a8..d3d3f1e7eb 100644 --- a/pybamm/models/submodels/particle/__init__.py +++ b/pybamm/models/submodels/particle/__init__.py @@ -4,3 +4,4 @@ from .fickian_single_particle_size_distribution import FickianSinglePSD from .fast_many_particles import FastManyParticles from .fast_single_particle import FastSingleParticle +from .fast_single_particle_size_distribution import FastSinglePSD \ No newline at end of file diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index 28a696f3c6..37874bd8fc 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -70,6 +70,68 @@ def _get_standard_flux_variables(self, N_s, N_s_xav): return variables + def _get_standard_concentration_distribution_variables(self, c_s): + """ + Forms standard concentration variables that depend on particle size R given + the input c_s_distribution. + """ + # Currently not possible to broadcast from (r, R) to (r, R, x) since + # domain x for broadcast is in "tertiary" position. + + if ( + c_s.domain == [self.domain.lower() + " particle-size domain"] + and c_s.auxiliary_domains["secondary"] != [self.domain.lower() + " electrode"] + ): + c_s_xav_distribution = pybamm.PrimaryBroadcast( + c_s, [self.domain.lower() + " particle"] + ) + + # Surface concentration distribution variables + c_s_surf_xav_distribution = c_s + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + elif c_s.domain == [self.domain.lower() + " particle"] and ( + c_s.auxiliary_domains["tertiary"] != [self.domain.lower() + " electrode"] + ): + c_s_xav_distribution = c_s + + # Surface concentration distribution variables + c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + + if self.domain == "Negative": + c_scale = self.param.c_n_max + elif self.domain == "Positive": + c_scale = self.param.c_p_max + + variables = { + "X-averaged " + + self.domain.lower() + + " particle concentration distribution": c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle concentration distribution " + + "[mol.m-3]": c_scale * c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration" + + " distribution": c_s_surf_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution " + + "[mol.m-3]": c_scale * c_s_surf_xav_distribution, + self.domain + + " particle surface concentration" + + " distribution": c_s_surf_distribution, + self.domain + + " particle surface concentration" + + " distribution [mol.m-3]": c_scale * c_s_surf_distribution, + } + return variables + def set_events(self, variables): c_s_surf = variables[self.domain + " particle surface concentration"] tol = 1e-4 diff --git a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py new file mode 100644 index 0000000000..9c68b2caaf --- /dev/null +++ b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py @@ -0,0 +1,179 @@ +# +# Class for a single particle-size distribution representing an +# electrode, with fast diffusion (uniform concentration in r) within particles +# +import pybamm + +from .base_particle import BaseParticle + + +class FastSinglePSD(BaseParticle): + """Class for molar conservation in a single (i.e., x-averaged) particle-size + distribution (PSD) with fast diffusion within each particle + (uniform concentration in r). + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + domain : str + The domain of the model either 'Negative' or 'Positive' + + + **Extends:** :class:`pybamm.particle.BaseParticle` + """ + + def __init__(self, param, domain): + super().__init__(param, domain) + # pybamm.citations.register("kirk2020") + + def get_fundamental_variables(self): + # The concentration is uniform throughout each particle, so we + # can just use the surface value. This avoids dealing with + # x, R *and* r averaged quantities, which may be confusing. + + if self.domain == "Negative": + # distribution variables + c_s_surf_xav_distribution = pybamm.Variable( + "X-averaged negative particle surface concentration distribution", + domain="negative particle-size domain", + auxiliary_domains={"secondary": "current collector"}, + bounds=(0, 1), + ) + R_variable = pybamm.standard_spatial_vars.R_variable_n + R_dim = self.param.R_n + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_n(R_variable) + + elif self.domain == "Positive": + # distribution variables + c_s_surf_xav_distribution = pybamm.Variable( + "X-averaged positive particle surface concentration distribution", + domain="positive particle-size domain", + auxiliary_domains={"secondary": "current collector"}, + bounds=(0, 1), + ) + R_variable = pybamm.standard_spatial_vars.R_variable_p + R_dim = self.param.R_p + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_p(R_variable) + + # Flux variables (zero) + N_s = pybamm.FullBroadcastToEdges( + 0, + [self.domain.lower() + " particle"], + auxiliary_domains={ + "secondary": self.domain.lower() + " electrode", + "tertiary": "current collector", + }, + ) + N_s_xav = pybamm.FullBroadcast( + 0, self.domain.lower() + " electrode", "current collector" + ) + + # Standard R-averaged variables + c_s_surf_xav = pybamm.Integral(f_a_dist * c_s_surf_xav_distribution, R_variable) + c_s_xav = pybamm.PrimaryBroadcast( + c_s_surf_xav, [self.domain.lower() + " particle"] + ) + c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) + variables = self._get_standard_concentration_variables(c_s, c_s_xav) + variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) + + # Standard distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables( + c_s_surf_xav_distribution + ) + ) + variables.update( + { + self.domain + " particle size": R_variable, + self.domain + " particle size [m]": R_variable * R_dim, + self.domain + + " area-weighted particle-size" + + " distribution": f_a_dist, + self.domain + + " area-weighted particle-size" + + " distribution [m]": f_a_dist / R_dim, + } + ) + return variables + + def set_rhs(self, variables): + c_s_surf_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution" + ] + j_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " electrode interfacial current density distribution" + ] + R_variable = variables[self.domain + " particle size"] + + if self.domain == "Negative": + self.rhs = { + c_s_surf_xav_distribution: -3 + * j_xav_distribution + / self.param.a_n + / R_variable + } + + elif self.domain == "Positive": + self.rhs = { + c_s_surf_xav_distribution: -3 + * j_xav_distribution + / self.param.a_p + / self.param.gamma_p + / R_variable + } + + def set_initial_conditions(self, variables): + """ + For single particle-size distribution models, initial conditions can't + depend on x so we arbitrarily set the initial values of the single + particles to be given by the values at x=0 in the negative electrode + and x=1 in the positive electrode. Typically, supplied initial + conditions are uniform x. + """ + c_s_surf_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution" + ] + + if self.domain == "Negative": + c_init = self.param.c_n_init(0) + + elif self.domain == "Positive": + c_init = self.param.c_p_init(1) + + self.initial_conditions = {c_s_surf_xav_distribution: c_init} + + def set_events(self, variables): + c_s_surf_xav_distribution = variables[ + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution" + ] + tol = 1e-4 + + self.events.append( + pybamm.Event( + "Minumum " + self.domain.lower() + " particle surface concentration", + pybamm.min(c_s_surf_xav_distribution) - tol, + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Maximum " + self.domain.lower() + " particle surface concentration", + (1 - tol) - pybamm.max(c_s_surf_xav_distribution), + pybamm.EventType.TERMINATION, + ) + ) diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py index 326358e331..165217f905 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -1,5 +1,6 @@ # -# Class for a single particle with Fickian diffusion +# Class for a single particle-size distribution representing an +# electrode, with Fickian diffusion within each particle # import pybamm @@ -7,8 +8,8 @@ class FickianSinglePSD(BaseParticle): - """Base class for molar conservation in a single x-averaged particle which employs - Fick's law. + """Class for molar conservation in a single (i.e., x-averaged) particle-size + distribution (PSD) with Fickian diffusion within each particle. Parameters ---------- @@ -23,6 +24,7 @@ class FickianSinglePSD(BaseParticle): def __init__(self, param, domain): super().__init__(param, domain) + # pybamm.citations.register("kirk2020") def get_fundamental_variables(self): if self.domain == "Negative": @@ -240,46 +242,3 @@ def set_events(self, variables): ) ) - def _get_standard_concentration_distribution_variables(self, c_s_xav_distribution): - ''' - Forms concentration variables that depend on particle size R given the input - c_s_distribution. - ''' - # Currently not possible to broadcast from (r, R) to (r, R, x) since - # domain x for broadcast is in "tertiary" position. - - # Surface concentration distribution variables - c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) - c_s_surf_distribution = pybamm.SecondaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] - ) - if self.domain == "Negative": - c_scale = self.param.c_n_max - elif self.domain == "Positive": - c_scale = self.param.c_p_max - - variables = { - "X-averaged " - + self.domain.lower() - + " particle concentration distribution": c_s_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle concentration distribution " - + "[mol.m-3]": c_scale * c_s_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle surface concentration" - + " distribution": c_s_surf_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle surface concentration distribution " - + "[mol.m-3]": c_scale * c_s_surf_xav_distribution, - self.domain - + " particle surface concentration" - + " distribution": c_s_surf_distribution, - self.domain - + " particle surface concentration" - + " distribution [mol.m-3]": c_scale * c_s_surf_distribution, - } - return variables - \ No newline at end of file From 9eb72e44661bc12f930762558ffd12ecf440164c Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 16 Jul 2020 16:55:33 +0100 Subject: [PATCH 12/67] Default geometry and meshes etc for PSDs --- pybamm/geometry/battery_geometry.py | 22 ++++++++++++- pybamm/geometry/standard_spatial_vars.py | 8 ++--- .../full_battery_models/base_battery_model.py | 11 +++++++ .../lithium_ion/__init__.py | 4 +-- .../lithium_ion/base_lithium_ion_model.py | 4 --- .../{basic_psd_model.py => basic_mpm.py} | 2 +- .../lithium_ion/{psd_model.py => mpm.py} | 32 ++++++++++++++----- 7 files changed, 63 insertions(+), 20 deletions(-) rename pybamm/models/full_battery_models/lithium_ion/{basic_psd_model.py => basic_mpm.py} (99%) rename pybamm/models/full_battery_models/lithium_ion/{psd_model.py => mpm.py} (92%) diff --git a/pybamm/geometry/battery_geometry.py b/pybamm/geometry/battery_geometry.py index c2b0f99f78..7f0ad85402 100644 --- a/pybamm/geometry/battery_geometry.py +++ b/pybamm/geometry/battery_geometry.py @@ -4,7 +4,11 @@ import pybamm -def battery_geometry(include_particles=True, current_collector_dimension=0): +def battery_geometry( + include_particles=True, + particle_size_distribution=False, + current_collector_dimension=0, +): """ A convenience function to create battery geometries. @@ -12,6 +16,8 @@ def battery_geometry(include_particles=True, current_collector_dimension=0): ---------- include_particles : bool Whether to include particle domains + particle_size_distribution : bool + Whether to include size domains for particle-size distributions current_collector_dimensions : int, default The dimensions of the current collector. Should be 0 (default), 1 or 2 @@ -38,6 +44,20 @@ def battery_geometry(include_particles=True, current_collector_dimension=0): "positive particle": {var.r_p: {"min": 0, "max": 1}}, } ) + # Add particle-size domains + if particle_size_distribution is True: + R_max_n = pybamm.Parameter("Negative maximum particle radius") + R_max_p = pybamm.Parameter("Positive maximum particle radius") + geometry.update( + { + "negative particle-size domain": { + var.R_variable_n: {"min": 0, "max": R_max_n} + }, + "positive particle-size domain": { + var.R_variable_p: {"min": 0, "max": R_max_p} + }, + } + ) if current_collector_dimension == 0: geometry["current collector"] = {var.z: {"position": 1}} diff --git a/pybamm/geometry/standard_spatial_vars.py b/pybamm/geometry/standard_spatial_vars.py index f040c968fd..d584fc2157 100644 --- a/pybamm/geometry/standard_spatial_vars.py +++ b/pybamm/geometry/standard_spatial_vars.py @@ -51,7 +51,7 @@ ) R_variable_n = pybamm.SpatialVariable( - "negative particle-size variable", + "R_n", domain=["negative particle-size domain"], # auxiliary_domains={ # "secondary": "negative electrode", @@ -60,7 +60,7 @@ coord_sys="cartesian", ) R_variable_p = pybamm.SpatialVariable( - "positive particle-size variable", + "R_p", domain=["positive particle-size domain"], # auxiliary_domains={ # "secondary": "positive electrode", @@ -122,12 +122,12 @@ ) R_variable_n_edge = pybamm.SpatialVariableEdge( - "negative particle-size variable", + "R_n", domain=["negative particle-size domain"], coord_sys="cartesian", ) R_variable_p_edge = pybamm.SpatialVariableEdge( - "positive particle-size variable", + "R_p", domain=["positive particle-size domain"], coord_sys="cartesian", ) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index dfd0184456..5cfaf6b5a6 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -113,6 +113,7 @@ def default_parameter_values(self): @property def default_geometry(self): return pybamm.battery_geometry( + particle_size_distribution=self.options["particle-size distribution"], current_collector_dimension=self.options["dimensionality"] ) @@ -127,6 +128,8 @@ def default_var_pts(self): var.r_p: 30, var.y: 10, var.z: 10, + var.R_variable_n: 50, + var.R_variable_p: 50, } # Reduce the default points for 2D current collectors if self.options["dimensionality"] == 2: @@ -141,6 +144,12 @@ def default_submesh_types(self): "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "negative particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "positive particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "negative particle-size domain": pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ), + "positive particle-size domain": pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ), } if self.options["dimensionality"] == 0: base_submeshes["current collector"] = pybamm.MeshGenerator(pybamm.SubMesh0D) @@ -160,6 +169,8 @@ def default_spatial_methods(self): "macroscale": pybamm.FiniteVolume(), "negative particle": pybamm.FiniteVolume(), "positive particle": pybamm.FiniteVolume(), + "negative particle-size domain": pybamm.FiniteVolume(), + "positive particle-size domain": pybamm.FiniteVolume(), } if self.options["dimensionality"] == 0: # 0D submesh - use base spatial method diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/pybamm/models/full_battery_models/lithium_ion/__init__.py index dd87822da6..37fb59d90f 100644 --- a/pybamm/models/full_battery_models/lithium_ion/__init__.py +++ b/pybamm/models/full_battery_models/lithium_ion/__init__.py @@ -7,5 +7,5 @@ from .dfn import DFN from .basic_dfn import BasicDFN from .basic_spm import BasicSPM -from .basic_psd_model import BasicPSDModel -from .psd_model import PSDModel +from .basic_MPM import BasicMPM +from .mpm import MPM diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index 9e44cf9d2d..2b73e8d418 100644 --- a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -46,10 +46,6 @@ def set_standard_output_variables(self): "r_n [m]": var.r_n * param.R_n, "r_p": var.r_p, "r_p [m]": var.r_p * param.R_p, - "Negative particle size": var.R_variable_n, - "Negative particle size [m]": var.R_variable_n * param.R_n, - "Positive particle size": var.R_variable_p, - "Positive particle size [m]": var.R_variable_p * param.R_p, } ) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py similarity index 99% rename from pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py rename to pybamm/models/full_battery_models/lithium_ion/basic_mpm.py index ed9748d656..8132530aab 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py @@ -5,7 +5,7 @@ from .base_lithium_ion_model import BaseModel -class BasicPSDModel(BaseModel): +class BasicMPM(BaseModel): """Particle-Size Distribution (PSD) model of a lithium-ion battery, from [1]_. This class is similar to the :class:`pybamm.lithium_ion.SPM` model class in that it diff --git a/pybamm/models/full_battery_models/lithium_ion/psd_model.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py similarity index 92% rename from pybamm/models/full_battery_models/lithium_ion/psd_model.py rename to pybamm/models/full_battery_models/lithium_ion/mpm.py index dbdb0278c0..bd907745d6 100644 --- a/pybamm/models/full_battery_models/lithium_ion/psd_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -158,7 +158,8 @@ def default_parameter_values(self): R_n_dim = default_params["Negative particle radius [m]"] R_p_dim = default_params["Positive particle radius [m]"] - # New parameter values + # Additional particle distribution parameter values + # Area-weighted standard deviations sd_a_n = 0.5 sd_a_p = 0.3 @@ -166,16 +167,21 @@ def default_parameter_values(self): sd_a_p_dim = sd_a_p * R_p_dim # Max radius in the particle-size distribution (dimensionless). - # Either 5 s.d.'s above the mean or the value 2, whichever is larger + # Either 5 s.d.'s above the mean or 2 times the mean, whichever is larger R_n_max = max(2, 1 + sd_a_n * 5) R_p_max = max(2, 1 + sd_a_p * 5) # lognormal area-weighted particle-size distribution def lognormal_distribution(R, R_av, sd): + ''' + A lognormal distribution with arguments + R : particle radius + R_av: mean particle radius + sd : standard deviation + (Inputs can be dimensional or dimensionless) + ''' import numpy as np - # inputs are particle radius R, the mean R_av, and standard deviation sd - # inputs can be dimensional or dimensionless mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) return ( @@ -184,12 +190,14 @@ def lognormal_distribution(R, R_av, sd): / (R) ) + # Set the (area-weighted) particle-size distributions (dimensional) def f_a_dist_n_dim(R): return lognormal_distribution(R, R_n_dim, sd_a_n_dim) def f_a_dist_p_dim(R): return lognormal_distribution(R, R_p_dim, sd_a_p_dim) + # Update default parameters default_params.update( { "Negative area-weighted particle-size standard deviation": sd_a_n, @@ -198,8 +206,10 @@ def f_a_dist_p_dim(R): "Positive area-weighted particle-size standard deviation": sd_a_p, "Positive area-weighted particle-size " + "standard deviation [m]": sd_a_p_dim, - "Negative maximum particle radius": R_n_max, + "Negative maximum particle radius": R_n_max , + "Negative maximum particle radius [m]": R_n_max * R_n_dim, "Positive maximum particle radius": R_p_max, + "Positive maximum particle radius [m]": R_p_max * R_p_dim, "Negative area-weighted " + "particle-size distribution [m]": f_a_dist_n_dim, "Positive area-weighted " @@ -209,6 +219,8 @@ def f_a_dist_p_dim(R): ) return default_params + +''' @property def default_geometry(self): default_geom = super().default_geometry @@ -235,7 +247,8 @@ def default_geometry(self): } ) return default_geom - +''' +''' @property def default_var_pts(self): defaults = super().default_var_pts @@ -246,7 +259,8 @@ def default_var_pts(self): # add to dictionary defaults.update({R_variable_n: 50, R_variable_p: 50}) return defaults - +''' +''' @property def default_submesh_types(self): default_submeshes = super().default_submesh_types @@ -262,7 +276,8 @@ def default_submesh_types(self): } ) return default_submeshes - +''' +''' @property def default_spatial_methods(self): default_spatials = super().default_spatial_methods @@ -274,3 +289,4 @@ def default_spatial_methods(self): } ) return default_spatials +''' From 0c2850f1b42a416775f5bddf98eff438f8c7d3b9 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 17 Jul 2020 17:12:07 +0100 Subject: [PATCH 13/67] added R-average --- pybamm/expression_tree/unary_operators.py | 59 ++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 7c7e0e6a9a..7d5df79b00 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1061,10 +1061,14 @@ def x_average(symbol): elif symbol.domain == ["negative electrode", "separator", "positive electrode"]: x = pybamm.standard_spatial_vars.x l = pybamm.Scalar(1) - elif symbol.domain == ["negative particle"]: + elif symbol.domain == ["negative particle"] or symbol.domain == [ + "negative particle-size domain" + ]: x = pybamm.standard_spatial_vars.x_n l = pybamm.geometric_parameters.l_n - elif symbol.domain == ["positive particle"]: + elif symbol.domain == ["positive particle"] or symbol.domain == [ + "positive particle-size domain" + ]: x = pybamm.standard_spatial_vars.x_p l = pybamm.geometric_parameters.l_p else: @@ -1183,6 +1187,57 @@ def r_average(symbol): return Integral(symbol, r) / Integral(v, r) +def R_average(symbol, domain): + """convenience function for averaging over particle size R. + + Parameters + ---------- + symbol : :class:`pybamm.Symbol` + The function to be averaged + domain : str + The electrode for averaging, either "negative" or "positive" + Returns + ------- + :class:`Symbol` + the new averaged symbol + """ + # Can't take average if the symbol evaluates on edges + if symbol.evaluates_on_edges("primary"): + raise ValueError("Can't take the R-average of a symbol that evaluates on edges") + + if domain.lower() not in ["negative", "positive"]: + raise ValueError( + """Electrode domain must be "positive" or "negative" not {}""".format( + domain.lower() + ) + ) + + if symbol.domain not in [ + ["negative particle-size domain"], + ["positive particle-size domain"], + ]: + raise pybamm.DomainError( + """R-average only implemented for primary 'particle size' domains, + but symbol has domains {}""".format( + symbol.domain + ) + ) + + # Define spatial variable with same domains as symbol + R = pybamm.SpatialVariable( + "R", + domain=symbol.domain, + auxiliary_domains=symbol.auxiliary_domains, + coord_sys="cartesian", + ) + if domain.lower() == "negative": + f_a_dist = pybamm.standard_parameters_lithium_ion.f_a_dist_n(R) + elif domain.lower() == "positive": + f_a_dist = pybamm.standard_parameters_lithium_ion.f_a_dist_p(R) + + return Integral(f_a_dist * symbol, R) + + def boundary_value(symbol, side): """convenience function for creating a :class:`pybamm.BoundaryValue` From 5f489dfd9c77c70a6be2a8e432c17f27a5fafbb4 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 17 Jul 2020 17:14:03 +0100 Subject: [PATCH 14/67] fied typos and style --- pybamm/expression_tree/broadcasts.py | 7 ++++--- pybamm/models/full_battery_models/base_battery_model.py | 4 ++-- pybamm/models/full_battery_models/lithium_ion/__init__.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pybamm/expression_tree/broadcasts.py b/pybamm/expression_tree/broadcasts.py index 75d899fab0..c7575c8a19 100644 --- a/pybamm/expression_tree/broadcasts.py +++ b/pybamm/expression_tree/broadcasts.py @@ -92,8 +92,9 @@ def check_and_set_domains( self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains ): "See :meth:`Broadcast.check_and_set_domains`" - # Can only do primary broadcast from current collector to electrode, particle-size or particle - # or from electrode to particle-size or particle. Note e.g. current collector to particle *is* allowed + # Can only do primary broadcast from current collector to electrode, + # particle-size or particle or from electrode to particle-size or particle. + # Note e.g. current collector to particle *is* allowed if child.domain == []: pass elif child.domain == ["current collector"] and broadcast_domain[0] not in [ @@ -126,7 +127,7 @@ def check_and_set_domains( elif child.domain[0] in [ "negative particle-size domain", "positive particle-size domain", - ] and broadcast_domain[0] not in ["negative particle", "positive particle",]: + ] and broadcast_domain[0] not in ["negative particle", "positive particle"]: raise pybamm.DomainError( """Primary broadcast from particle-size domain must be to particle domain""" diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 5cfaf6b5a6..b34e814403 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -128,8 +128,8 @@ def default_var_pts(self): var.r_p: 30, var.y: 10, var.z: 10, - var.R_variable_n: 50, - var.R_variable_p: 50, + var.R_variable_n: 30, + var.R_variable_p: 30, } # Reduce the default points for 2D current collectors if self.options["dimensionality"] == 2: diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/pybamm/models/full_battery_models/lithium_ion/__init__.py index 37fb59d90f..fb9aad0ba5 100644 --- a/pybamm/models/full_battery_models/lithium_ion/__init__.py +++ b/pybamm/models/full_battery_models/lithium_ion/__init__.py @@ -7,5 +7,5 @@ from .dfn import DFN from .basic_dfn import BasicDFN from .basic_spm import BasicSPM -from .basic_MPM import BasicMPM +from .basic_mpm import BasicMPM from .mpm import MPM From 5d938a8509c77d331464c7a23c82031dac72b71b Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 17 Jul 2020 17:31:10 +0100 Subject: [PATCH 15/67] added ManyPSD submodels, and the option to DFN --- .../full_battery_models/lithium_ion/dfn.py | 44 ++- .../full_battery_models/lithium_ion/mpm.py | 9 +- .../submodels/interface/base_interface.py | 65 +---- pybamm/models/submodels/particle/__init__.py | 1 + .../submodels/particle/base_particle.py | 51 +++- ...ickian_many_particle_size_distributions.py | 252 ++++++++++++++++++ 6 files changed, 343 insertions(+), 79 deletions(-) create mode 100644 pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index 07d4251e6c..793ec7436e 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -78,20 +78,36 @@ def set_interfacial_submodel(self): def set_particle_submodel(self): - if self.options["particle"] == "Fickian diffusion": - self.submodels["negative particle"] = pybamm.particle.FickianManyParticles( - self.param, "Negative" - ) - self.submodels["positive particle"] = pybamm.particle.FickianManyParticles( - self.param, "Positive" - ) - elif self.options["particle"] == "fast diffusion": - self.submodels["negative particle"] = pybamm.particle.FastManyParticles( - self.param, "Negative" - ) - self.submodels["positive particle"] = pybamm.particle.FastManyParticles( - self.param, "Positive" - ) + if self.options["particle-size distribution"]: + if self.options["particle"] == "Fickian diffusion": + self.submodels["negative particle"] = pybamm.particle.FickianManyPSDs( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FickianManyPSDs( + self.param, "Positive" + ) + elif self.options["particle"] == "fast diffusion": + self.submodels["negative particle"] = pybamm.particle.FastManyPSDs( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FastManyPSDs( + self.param, "Positive" + ) + else: + if self.options["particle"] == "Fickian diffusion": + self.submodels[ + "negative particle" + ] = pybamm.particle.FickianManyParticles(self.param, "Negative") + self.submodels[ + "positive particle" + ] = pybamm.particle.FickianManyParticles(self.param, "Positive") + elif self.options["particle"] == "fast diffusion": + self.submodels[ + "negative particle" + ] = pybamm.particle.FastManyParticles(self.param, "Negative") + self.submodels[ + "positive particle" + ] = pybamm.particle.FastManyParticles(self.param, "Positive") def set_solid_submodel(self): diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index bd907745d6..45b75da39b 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -1,12 +1,13 @@ # -# Single Particle Model (SPM) +# Many-Particle Model (MPM) # import pybamm from .base_lithium_ion_model import BaseModel -class PSDModel(BaseModel): - """Particle-Size Distribution (PSD) Model of a lithium-ion battery, from [1]_. +class MPM(BaseModel): + """Many-Particle Model (MPM) of a lithium-ion battery with particle-size + distributions for each electrode, from [1]_. Parameters ---------- @@ -31,7 +32,7 @@ class PSDModel(BaseModel): """ def __init__( - self, options=None, name="Particle-Size Distribution Model", build=True + self, options=None, name="Many-Particle Model", build=True ): super().__init__(options, name) self.options["particle-size distribution"] = True diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 6f9b816e2f..32efd98bcd 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -400,15 +400,8 @@ def _get_standard_exchange_current_variables(self, j0): # If j0 depends on particle size R then must R-average to get standard # output exchange current density if j0.domain == [self.domain.lower() + " particle-size domain"]: - if self.domain == "Negative": - R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable) - elif self.domain == "Positive": - R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable) - # R-average - j0 = pybamm.Integral(f_a_dist * j0, R_variable) + j0 = pybamm.R_average(j0, self.domain) # X-average, and broadcast if necessary if j0.domain == []: @@ -494,15 +487,8 @@ def _get_standard_overpotential_variables(self, eta_r): # If eta_r depends on particle size R then must R-average to get standard # output reaction overpotential if eta_r.domain == [self.domain.lower() + " particle-size domain"]: - if self.domain == "Negative": - R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable) - elif self.domain == "Positive": - R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable) - # R-average - eta_r = pybamm.Integral(f_a_dist * eta_r, R_variable) + eta_r = pybamm.R_average(eta_r, self.domain) # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) @@ -617,15 +603,8 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # If ocp depends on particle size R then must R-average to get standard # output open circuit potential if ocp.domain == [self.domain.lower() + " particle-size domain"]: - if self.domain == "Negative": - R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable) - elif self.domain == "Positive": - R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable) - # R-average - ocp = pybamm.Integral(f_a_dist * ocp, R_variable) + ocp = pybamm.R_average(ocp, self.domain) # X-average, and broadcast if necessary if ocp.domain == []: @@ -642,15 +621,8 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # If dUdT depends on particle size R then must R-average to get standard # output entropic change if dUdT.domain == [self.domain.lower() + " particle-size domain"]: - if self.domain == "Negative": - R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable) - elif self.domain == "Positive": - R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable) - # R-average - dUdT = pybamm.Integral(f_a_dist * dUdT, R_variable) + dUdT = pybamm.R_average(dUdT, self.domain) dUdT_av = pybamm.x_average(dUdT) @@ -699,25 +671,21 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): particle size for "particle-size distribution" models, and the standard R-averaged current density (j) """ - # T must have same domains as j0, eta_r, so reverse any broadcast to - # "electrode" then broadcast onto "particle-size domain" - if isinstance(T, pybamm.Broadcast): - T = T.orphans[0] + # T must have same domains as j0, eta_r, so remove electrode domain from T + # if necessary (only check eta_r, as j0 should already match) + if eta_r.domains["secondary"] != [self.domain.lower() + " electrode"]: + T = pybamm.x_average(T) + + # Broadcast T onto "particle-size domain" T = pybamm.PrimaryBroadcast( T, [self.domain.lower() + " particle-size domain"] ) # current density that depends on particle size R j_distribution = self._get_kinetics(j0, ne, eta_r, T) - if self.domain == "Negative": - R_variable = pybamm.standard_spatial_vars.R_variable_n - f_a_dist = self.param.f_a_dist_n(R_variable) - elif self.domain == "Positive": - R_variable = pybamm.standard_spatial_vars.R_variable_p - f_a_dist = self.param.f_a_dist_p(R_variable) # R-average - j = pybamm.Integral(f_a_dist * j_distribution, R_variable) + j = pybamm.R_average(j_distribution, self.domain) return j, j_distribution def _get_standard_PSD_interfacial_current_variables(self, j_distribution): @@ -726,16 +694,9 @@ def _get_standard_PSD_interfacial_current_variables(self, j_distribution): relevant if "particle-size distribution" option is True. """ # X-average and broadcast if necessary - if self.domain.lower() + " electrode" in j_distribution.auxiliary_domains: - - if self.domain == "Negative": - l = self.param.l_n - x = pybamm.standard_spatial_vars.x_n - elif self.domain == "Positive": - l = self.param.l_p - x = pybamm.standard_spatial_vars.x_p + if j_distribution.domains["secondary"] == [self.domain.lower() + " electrode"]: # x-average - j_xav_distribution = pybamm.Integral(j_distribution, x) / l + j_xav_distribution = pybamm.x_average(j_distribution) else: j_xav_distribution = j_distribution j_distribution = pybamm.SecondaryBroadcast( diff --git a/pybamm/models/submodels/particle/__init__.py b/pybamm/models/submodels/particle/__init__.py index d3d3f1e7eb..ec21feb0df 100644 --- a/pybamm/models/submodels/particle/__init__.py +++ b/pybamm/models/submodels/particle/__init__.py @@ -1,5 +1,6 @@ from .base_particle import BaseParticle from .fickian_many_particles import FickianManyParticles +from .fickian_many_particle_size_distributions import FickianManyPSDs from .fickian_single_particle import FickianSingleParticle from .fickian_single_particle_size_distribution import FickianSinglePSD from .fast_many_particles import FastManyParticles diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index 37874bd8fc..65a76360c3 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -75,16 +75,32 @@ def _get_standard_concentration_distribution_variables(self, c_s): Forms standard concentration variables that depend on particle size R given the input c_s_distribution. """ - # Currently not possible to broadcast from (r, R) to (r, R, x) since + if self.domain == "Negative": + c_scale = self.param.c_n_max + elif self.domain == "Positive": + c_scale = self.param.c_p_max + + # Note: Currently not possible to broadcast from (r, R) to (r, R, x) since # domain x for broadcast is in "tertiary" position. - if ( - c_s.domain == [self.domain.lower() + " particle-size domain"] - and c_s.auxiliary_domains["secondary"] != [self.domain.lower() + " electrode"] - ): + # Broadcast and x-average when necessary + if c_s.domain == [ + self.domain.lower() + " particle-size domain" + ] and c_s.auxiliary_domains["secondary"] != [ + self.domain.lower() + " electrode" + ]: c_s_xav_distribution = pybamm.PrimaryBroadcast( c_s, [self.domain.lower() + " particle"] ) + # Placeholder broadcast to x, filled with zeros only + c_s_distribution = pybamm.FullBroadcast( + 0, + [self.domain.lower() + " particle"], + { + "secondary": self.domain.lower() + " particle-size domain", + "tertiary": self.domain.lower() + " electrode", + }, + ) # Surface concentration distribution variables c_s_surf_xav_distribution = c_s @@ -95,19 +111,36 @@ def _get_standard_concentration_distribution_variables(self, c_s): c_s.auxiliary_domains["tertiary"] != [self.domain.lower() + " electrode"] ): c_s_xav_distribution = c_s + # Placeholder broadcast to x, filled with zeros only + c_s_distribution = pybamm.FullBroadcast( + 0, + [self.domain.lower() + " particle"], + { + "secondary": self.domain.lower() + " particle-size domain", + "tertiary": self.domain.lower() + " electrode", + }, + ) # Surface concentration distribution variables c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) c_s_surf_distribution = pybamm.SecondaryBroadcast( c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] ) + else: + c_s_distribution = c_s + # TODO: x-average the *tertiary* domain. Leave unaltered for now. + c_s_xav_distribution = c_s - if self.domain == "Negative": - c_scale = self.param.c_n_max - elif self.domain == "Positive": - c_scale = self.param.c_p_max + # Surface concentration distribution variables + c_s_surf_distribution = pybamm.surf(c_s) + c_s_surf_xav_distribution = pybamm.x_average(c_s_surf_distribution) variables = { + self.domain + + " particle concentration distribution": c_s_distribution, + self.domain + + " particle concentration distribution " + + "[mol.m-3]": c_scale * c_s_distribution, "X-averaged " + self.domain.lower() + " particle concentration distribution": c_s_xav_distribution, diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py new file mode 100644 index 0000000000..639c2ad51b --- /dev/null +++ b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py @@ -0,0 +1,252 @@ +# +# Class for a many particle-size distributions, one distribution at every +# x location of the electrode, and Fickian diffusion within each particle +# +import pybamm + +from .base_particle import BaseParticle + + +class FickianManyPSDs(BaseParticle): + """Class for molar conservation in a single (i.e., x-averaged) particle-size + distribution (PSD) with Fickian diffusion within each particle. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + domain : str + The domain of the model either 'Negative' or 'Positive' + + + **Extends:** :class:`pybamm.particle.BaseParticle` + """ + + def __init__(self, param, domain): + super().__init__(param, domain) + # pybamm.citations.register("kirk2020") + + def get_fundamental_variables(self): + if self.domain == "Negative": + # distribution variables + c_s_distribution = pybamm.Variable( + "Negative particle concentration distribution", + domain="negative particle", + auxiliary_domains={ + "secondary": "negative particle-size domain", + "tertiary": "negative electrode", + }, + bounds=(0, 1), + ) + R = pybamm.standard_spatial_vars.R_variable_n + R_variable = pybamm.SecondaryBroadcast(R, ["negative electrode"]) + R_dim = self.param.R_n + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_n(R_variable) + + elif self.domain == "Positive": + # distribution variables + c_s_distribution = pybamm.Variable( + "Positive particle concentration distribution", + domain="positive particle", + auxiliary_domains={ + "secondary": "positive particle-size domain", + "tertiary": "positive electrode", + }, + bounds=(0, 1), + ) + R = pybamm.standard_spatial_vars.R_variable_p + R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) + R_dim = self.param.R_p + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_p(R_variable) + + # Standard R-averaged variables + c_s = pybamm.Integral(f_a_dist * c_s_distribution, R) + c_s_xav = pybamm.x_average(c_s) + variables = self._get_standard_concentration_variables(c_s, c_s_xav) + + # Standard distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables(c_s_distribution) + ) + variables.update( + { + self.domain + " particle size": R_variable, + self.domain + " particle size [m]": R_variable * R_dim, + self.domain + + " area-weighted particle-size" + + " distribution": pybamm.x_average(f_a_dist), + self.domain + + " area-weighted particle-size" + + " distribution [m]": pybamm.x_average(f_a_dist) / R_dim, + } + ) + return variables + + def get_coupled_variables(self, variables): + c_s_distribution = variables[ + self.domain + " particle concentration distribution" + ] + R_variable = variables[self.domain + " particle size"] + + # broadcast to particle-size domain then again into particle + T_k = pybamm.PrimaryBroadcast( + variables[self.domain + " electrode temperature"], + [self.domain.lower() + " particle-size domain"], + ) + T_k = pybamm.PrimaryBroadcast(T_k, [self.domain.lower() + " particle"],) + + if self.domain == "Negative": + N_s_distribution = ( + -self.param.D_n(c_s_distribution, T_k) + * pybamm.grad(c_s_distribution) + / R_variable + ) + f_a_dist = self.param.f_a_dist_n(R_variable) + + # spatial var to use in R integral below (cannot use R_variable as + # it is a broadcast) + R = pybamm.standard_spatial_vars.R_variable_n + elif self.domain == "Positive": + N_s_distribution = ( + -self.param.D_p(c_s_distribution, T_k) + * pybamm.grad(c_s_distribution) + / R_variable + ) + f_a_dist = self.param.f_a_dist_p(R_variable) + + # spatial var to use in R integral below (cannot use R_variable as + # it is a broadcast) + R = pybamm.standard_spatial_vars.R_variable_p + + # Standard R-averaged flux variables + N_s = pybamm.Integral(f_a_dist * N_s_distribution, R) + variables.update(self._get_standard_flux_variables(N_s, N_s)) + + # Standard distribution flux variables (R-dependent) + variables.update( + {self.domain + " particle flux distribution": N_s_distribution} + ) + return variables + + def set_rhs(self, variables): + c_s_distribution = variables[ + self.domain + " particle concentration distribution" + ] + + N_s_distribution = variables[self.domain + " particle flux distribution"] + + R_variable = variables[self.domain + " particle size"] + if self.domain == "Negative": + self.rhs = { + c_s_distribution: -(1 / self.param.C_n) + * pybamm.div(N_s_distribution) + / R_variable + } + elif self.domain == "Positive": + self.rhs = { + c_s_distribution: -(1 / self.param.C_p) + * pybamm.div(N_s_distribution) + / R_variable + } + + def set_boundary_conditions(self, variables): + # Extract variables + c_s_distribution = variables[ + self.domain + " particle concentration distribution" + ] + c_s_surf_distribution = variables[ + self.domain + " particle surface concentration distribution" + ] + j_distribution = variables[ + self.domain + " electrode interfacial current density distribution" + ] + R_variable = variables[self.domain + " particle size"] + + # Extract T and broadcast to particle-size domain + T_k = variables[self.domain + " electrode temperature"] + T_k = pybamm.PrimaryBroadcast( + T_k, [self.domain.lower() + " particle-size domain"] + ) + + # Set surface Neumann boundary values + if self.domain == "Negative": + rbc = ( + -self.param.C_n + * R_variable + * j_distribution + / self.param.a_n + / self.param.D_n(c_s_surf_distribution, T_k) + ) + + elif self.domain == "Positive": + rbc = ( + -self.param.C_p + * R_variable + * j_distribution + / self.param.a_p + / self.param.gamma_p + / self.param.D_p(c_s_surf_distribution, T_k) + ) + + self.boundary_conditions = { + c_s_distribution: { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (rbc, "Neumann"), + } + } + + def set_initial_conditions(self, variables): + """ + For single particle-size distribution models, initial conditions can't + depend on x so we arbitrarily set the initial values of the single + particles to be given by the values at x=0 in the negative electrode + and x=1 in the positive electrode. Typically, supplied initial + conditions are uniform x. + """ + c_s_distribution = variables[ + self.domain + " particle concentration distribution" + ] + + if self.domain == "Negative": + # Broadcast x_n to particle-size then into the particles + x_n = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_n, "negative particle-size domain" + ) + x_n = pybamm.PrimaryBroadcast(x_n, "negative particle") + c_init = self.param.c_n_init(x_n) + + elif self.domain == "Positive": + # Broadcast x_n to particle-size then into the particles + x_p = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_p, "positive particle-size domain" + ) + x_p = pybamm.PrimaryBroadcast(x_p, "positive particle") + c_init = self.param.c_p_init(x_p) + + self.initial_conditions = {c_s_distribution: c_init} + + def set_events(self, variables): + c_s_surf_distribution = variables[ + self.domain + " particle surface concentration distribution" + ] + tol = 1e-4 + + self.events.append( + pybamm.Event( + "Minumum " + self.domain.lower() + " particle surface concentration", + pybamm.min(c_s_surf_distribution) - tol, + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Maximum " + self.domain.lower() + " particle surface concentration", + (1 - tol) - pybamm.max(c_s_surf_distribution), + pybamm.EventType.TERMINATION, + ) + ) From 6e41c077801be933d3e6965bc90b8798c1c91a6c Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 17 Jul 2020 17:33:03 +0100 Subject: [PATCH 16/67] added option to use SPM as MPM --- .../full_battery_models/lithium_ion/spm.py | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index 71e41d14bb..e6dfb5dfb5 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -69,7 +69,10 @@ def set_convection_submodel(self): def set_interfacial_submodel(self): - if self.options["surface form"] is False: + if ( + self.options["surface form"] is False + and self.options["particle-size distribution"] is False + ): self.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( self.param, "Negative", "lithium-ion main", self.options ) @@ -97,32 +100,62 @@ def set_interfacial_submodel(self): def set_particle_submodel(self): - if self.options["particle"] == "Fickian diffusion": - self.submodels["negative particle"] = pybamm.particle.FickianSingleParticle( + if self.options["particle-size distribution"]: + if self.options["particle"] == "Fickian diffusion": + self.submodels["negative particle"] = pybamm.particle.FickianSinglePSD( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FickianSinglePSD( + self.param, "Positive" + ) + elif self.options["particle"] == "fast diffusion": + self.submodels["negative particle"] = pybamm.particle.FastSinglePSD( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FastSinglePSD( + self.param, "Positive" + ) + else: + if self.options["particle"] == "Fickian diffusion": + self.submodels[ + "negative particle" + ] = pybamm.particle.FickianSingleParticle(self.param, "Negative") + self.submodels[ + "positive particle" + ] = pybamm.particle.FickianSingleParticle(self.param, "Positive") + elif self.options["particle"] == "fast diffusion": + self.submodels[ + "negative particle" + ] = pybamm.particle.FastSingleParticle(self.param, "Negative") + self.submodels[ + "positive particle" + ] = pybamm.particle.FastSingleParticle(self.param, "Positive") + + def set_negative_electrode_submodel(self): + + if self.options["particle-size distribution"]: + self.submodels[ + "negative electrode" + ] = pybamm.electrode.ohm.LeadingOrderSizeDistribution( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.FickianSingleParticle( - self.param, "Positive" - ) - elif self.options["particle"] == "fast diffusion": - self.submodels["negative particle"] = pybamm.particle.FastSingleParticle( + else: + self.submodels["negative electrode"] = pybamm.electrode.ohm.LeadingOrder( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.FastSingleParticle( - self.param, "Positive" - ) - - def set_negative_electrode_submodel(self): - - self.submodels["negative electrode"] = pybamm.electrode.ohm.LeadingOrder( - self.param, "Negative" - ) def set_positive_electrode_submodel(self): - self.submodels["positive electrode"] = pybamm.electrode.ohm.LeadingOrder( - self.param, "Positive" - ) + if self.options["particle-size distribution"]: + self.submodels[ + "positive electrode" + ] = pybamm.electrode.ohm.LeadingOrderSizeDistribution( + self.param, "Positive" + ) + else: + self.submodels["positive electrode"] = pybamm.electrode.ohm.LeadingOrder( + self.param, "Positive" + ) def set_electrolyte_submodel(self): From 61ee8542833af7013d94f2035decb3ed82c99be4 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 17 Jul 2020 18:11:19 +0100 Subject: [PATCH 17/67] added PSD with fast diffusion submodel to DFN --- pybamm/models/submodels/particle/__init__.py | 1 + .../submodels/particle/base_particle.py | 22 ++- .../fast_many_particle_size_distributions.py | 185 ++++++++++++++++++ ...ickian_many_particle_size_distributions.py | 14 +- 4 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 pybamm/models/submodels/particle/fast_many_particle_size_distributions.py diff --git a/pybamm/models/submodels/particle/__init__.py b/pybamm/models/submodels/particle/__init__.py index ec21feb0df..7234aef99f 100644 --- a/pybamm/models/submodels/particle/__init__.py +++ b/pybamm/models/submodels/particle/__init__.py @@ -4,5 +4,6 @@ from .fickian_single_particle import FickianSingleParticle from .fickian_single_particle_size_distribution import FickianSinglePSD from .fast_many_particles import FastManyParticles +from .fast_many_particle_size_distributions import FastManyPSDs from .fast_single_particle import FastSingleParticle from .fast_single_particle_size_distribution import FastSinglePSD \ No newline at end of file diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index 65a76360c3..4a5f6b7f3e 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -73,7 +73,7 @@ def _get_standard_flux_variables(self, N_s, N_s_xav): def _get_standard_concentration_distribution_variables(self, c_s): """ Forms standard concentration variables that depend on particle size R given - the input c_s_distribution. + one concentration distribution variable c_s. """ if self.domain == "Negative": c_scale = self.param.c_n_max @@ -126,6 +126,26 @@ def _get_standard_concentration_distribution_variables(self, c_s): c_s_surf_distribution = pybamm.SecondaryBroadcast( c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] ) + elif c_s.domain == [ + self.domain.lower() + " particle-size domain" + ] and c_s.auxiliary_domains["secondary"] == [ + self.domain.lower() + " electrode" + ]: + c_s_surf_distribution = c_s + c_s_surf_xav_distribution = pybamm.x_average(c_s) + + c_s_xav_distribution = pybamm.PrimaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " particle"] + ) + # Placeholder broadcast to x, filled with zeros only + c_s_distribution = pybamm.FullBroadcast( + 0, + [self.domain.lower() + " particle"], + { + "secondary": self.domain.lower() + " particle-size domain", + "tertiary": self.domain.lower() + " electrode", + }, + ) else: c_s_distribution = c_s # TODO: x-average the *tertiary* domain. Leave unaltered for now. diff --git a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py new file mode 100644 index 0000000000..bd6f5e529b --- /dev/null +++ b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py @@ -0,0 +1,185 @@ +# +# Class for many particle-size distributions, one distribution at every +# x location of the electrode, with fast diffusion (uniform concentration in r) +# within particles +# +import pybamm + +from .base_particle import BaseParticle + + +class FastManyPSDs(BaseParticle): + """Class for molar conservation in many particle-size + distributions (PSD), one distribution at every x location of the electrode, + with fast diffusion (uniform concentration in r) within the particles + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + domain : str + The domain of the model either 'Negative' or 'Positive' + + + **Extends:** :class:`pybamm.particle.BaseParticle` + """ + + def __init__(self, param, domain): + super().__init__(param, domain) + # pybamm.citations.register("kirk2020") + + def get_fundamental_variables(self): + # The concentration is uniform throughout each particle, so we + # can just use the surface value. This avoids dealing with + # x, R *and* r averaged quantities, which may be confusing. + + if self.domain == "Negative": + # distribution variables + c_s_surf_distribution = pybamm.Variable( + "Negative particle surface concentration distribution", + domain="negative particle-size domain", + auxiliary_domains={ + "secondary": "negative electrode", + "tertiary": "current collector", + }, + bounds=(0, 1), + ) + R = pybamm.standard_spatial_vars.R_variable_n + R_variable = pybamm.SecondaryBroadcast(R, ["negative electrode"]) + R_dim = self.param.R_n + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_n(R_variable) + + elif self.domain == "Positive": + # distribution variables + c_s_surf_distribution = pybamm.Variable( + "Positive particle surface concentration distribution", + domain="positive particle-size domain", + auxiliary_domains={ + "secondary": "positive electrode", + "tertiary": "current collector", + }, + bounds=(0, 1), + ) + R = pybamm.standard_spatial_vars.R_variable_p + R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) + R_dim = self.param.R_p + + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_p(R_variable) + + # Flux variables (zero) + N_s = pybamm.FullBroadcastToEdges( + 0, + [self.domain.lower() + " particle"], + auxiliary_domains={ + "secondary": self.domain.lower() + " electrode", + "tertiary": "current collector", + }, + ) + N_s_xav = pybamm.FullBroadcast( + 0, self.domain.lower() + " electrode", "current collector" + ) + + # Standard R-averaged variables + c_s_surf = pybamm.Integral(f_a_dist * c_s_surf_distribution, R) + c_s = pybamm.PrimaryBroadcast( + c_s_surf, [self.domain.lower() + " particle"] + ) + c_s_xav = pybamm.x_average(c_s) + variables = self._get_standard_concentration_variables(c_s, c_s_xav) + variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) + + # Standard distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables( + c_s_surf_distribution + ) + ) + variables.update( + { + self.domain + " particle size": R_variable, + self.domain + " particle size [m]": R_variable * R_dim, + self.domain + + " area-weighted particle-size" + + " distribution": pybamm.x_average(f_a_dist), + self.domain + + " area-weighted particle-size" + + " distribution [m]": pybamm.x_average(f_a_dist) / R_dim, + } + ) + return variables + + def set_rhs(self, variables): + c_s_surf_distribution = variables[ + self.domain + + " particle surface concentration distribution" + ] + j_distribution = variables[ + self.domain + + " electrode interfacial current density distribution" + ] + R_variable = variables[self.domain + " particle size"] + + if self.domain == "Negative": + self.rhs = { + c_s_surf_distribution: -3 + * j_distribution + / self.param.a_n + / R_variable + } + + elif self.domain == "Positive": + self.rhs = { + c_s_surf_distribution: -3 + * j_distribution + / self.param.a_p + / self.param.gamma_p + / R_variable + } + + def set_initial_conditions(self, variables): + c_s_surf_distribution = variables[ + self.domain + + " particle surface concentration distribution" + ] + + if self.domain == "Negative": + # Broadcast x_n to particle-size domain + x_n = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_n, "negative particle-size domain" + ) + c_init = self.param.c_n_init(x_n) + + elif self.domain == "Positive": + # Broadcast x_p to particle-size domain + x_p = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_p, "positive particle-size domain" + ) + c_init = self.param.c_p_init(x_p) + + self.initial_conditions = {c_s_surf_distribution: c_init} + + def set_events(self, variables): + c_s_surf_distribution = variables[ + self.domain + + " particle surface concentration distribution" + ] + tol = 1e-4 + + self.events.append( + pybamm.Event( + "Minumum " + self.domain.lower() + " particle surface concentration", + pybamm.min(c_s_surf_distribution) - tol, + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Maximum " + self.domain.lower() + " particle surface concentration", + (1 - tol) - pybamm.max(c_s_surf_distribution), + pybamm.EventType.TERMINATION, + ) + ) diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py index 639c2ad51b..d9405bd9fc 100644 --- a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py @@ -1,5 +1,5 @@ # -# Class for a many particle-size distributions, one distribution at every +# Class for many particle-size distributions, one distribution at every # x location of the electrode, and Fickian diffusion within each particle # import pybamm @@ -8,8 +8,9 @@ class FickianManyPSDs(BaseParticle): - """Class for molar conservation in a single (i.e., x-averaged) particle-size - distribution (PSD) with Fickian diffusion within each particle. + """Class for molar conservation in many particle-size + distributions (PSD), one distribution at every x location of the electrode, + with Fickian diffusion within each particle. Parameters ---------- @@ -200,13 +201,6 @@ def set_boundary_conditions(self, variables): } def set_initial_conditions(self, variables): - """ - For single particle-size distribution models, initial conditions can't - depend on x so we arbitrarily set the initial values of the single - particles to be given by the values at x=0 in the negative electrode - and x=1 in the positive electrode. Typically, supplied initial - conditions are uniform x. - """ c_s_distribution = variables[ self.domain + " particle concentration distribution" ] From af48cf0044b9221868efa2a2a279222a99307f2b Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 30 Jul 2020 15:46:20 +0100 Subject: [PATCH 18/67] add distribution parameters to Marquis2019 set --- pybamm/geometry/battery_geometry.py | 10 +++-- ...te_lognormal_particle_size_distribution.py | 30 +++++++++++++ .../parameters.csv | 3 ++ ...o2_lognormal_particle_size_distribution.py | 30 +++++++++++++ .../cathodes/lico2_Marquis2019/parameters.csv | 3 ++ .../full_battery_models/lithium_ion/mpm.py | 45 +++++++++---------- .../fast_many_particle_size_distributions.py | 8 ++-- .../fast_single_particle_size_distribution.py | 2 +- ...ickian_many_particle_size_distributions.py | 2 +- ...ckian_single_particle_size_distribution.py | 2 +- pybamm/parameters/geometric_parameters.py | 12 +++++ .../standard_parameters_lithium_ion.py | 4 +- 12 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py create mode 100644 pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py diff --git a/pybamm/geometry/battery_geometry.py b/pybamm/geometry/battery_geometry.py index 7f0ad85402..04dbc18aff 100644 --- a/pybamm/geometry/battery_geometry.py +++ b/pybamm/geometry/battery_geometry.py @@ -46,15 +46,17 @@ def battery_geometry( ) # Add particle-size domains if particle_size_distribution is True: - R_max_n = pybamm.Parameter("Negative maximum particle radius") - R_max_p = pybamm.Parameter("Positive maximum particle radius") + R_min_n = pybamm.geometric_parameters.R_min_n + R_min_p = pybamm.geometric_parameters.R_min_p + R_max_n = pybamm.geometric_parameters.R_max_n + R_max_p = pybamm.geometric_parameters.R_max_p geometry.update( { "negative particle-size domain": { - var.R_variable_n: {"min": 0, "max": R_max_n} + var.R_variable_n: {"min": R_min_n, "max": R_max_n} }, "positive particle-size domain": { - var.R_variable_p: {"min": 0, "max": R_max_p} + var.R_variable_p: {"min": R_min_p, "max": R_max_p} }, } ) diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py b/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py new file mode 100644 index 0000000000..b82cb8d0af --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py @@ -0,0 +1,30 @@ +import pybamm +import numpy as np + + +def graphite_lognormal_particle_size_distribution(R): + """ + A lognormal particle-size distribution as a function of particle radius R. The mean + of the distribution is equal to the "Partice radius [m]" from the parameter set, + and the standard deviation is 0.3 times the mean. + + Parameters + ---------- + R : :class:`pybamm.Symbol` + Particle radius [m] + + """ + # Mean radius (dimensional) + R_av = 1E-5 + + # Standard deviation (dimensional) + sd = R_av * 0.3 + + # calculate usual lognormal parameters + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / (R) + ) diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv b/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv index 920c51c12a..2ae2722093 100644 --- a/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv @@ -12,6 +12,9 @@ Negative electrode porosity,0.3,Scott Moura FastDFN,electrolyte volume fraction Negative electrode active material volume fraction,0.7,,assuming zero binder volume fraction Negative particle radius [m],1E-05,Scott Moura FastDFN, Negative particle distribution in x,1,, +Negative minimum particle radius [m],0,, +Negative maximum particle radius [m],2.5E-05,, +Negative area-weighted particle-size distribution [m-1],[function]graphite_lognormal_particle_size_distribution Negative electrode surface area to volume ratio [m-1],180000,Scott Moura FastDFN, Negative electrode Bruggeman coefficient (electrolyte),1.5,Scott Moura FastDFN, Negative electrode Bruggeman coefficient (electrode),1.5,Scott Moura FastDFN, diff --git a/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py b/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py new file mode 100644 index 0000000000..17fe9df13e --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py @@ -0,0 +1,30 @@ +import pybamm +import numpy as np + + +def lico2_lognormal_particle_size_distribution(R): + """ + A lognormal particle-size distribution as a function of particle radius R. The mean + of the distribution is equal to the "Partice radius [m]" from the parameter set, + and the standard deviation is 0.3 times the mean. + + Parameters + ---------- + R : :class:`pybamm.Symbol` + Particle radius [m] + + """ + # Mean radius (dimensional) + R_av = 1E-5 + + # Standard deviation (dimensional) + sd = R_av * 0.3 + + # calculate usual lognormal parameters + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / (R) + ) diff --git a/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv b/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv index bfac949bd7..a4a26d2eb4 100644 --- a/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv @@ -12,6 +12,9 @@ Positive electrode porosity,0.3,Scott Moura FastDFN,electrolyte volume fraction Positive electrode active material volume fraction,0.7,,assuming zero binder volume fraction Positive particle radius [m],1E-05,Scott Moura FastDFN, Positive particle distribution in x,1,, +Positive minimum particle radius [m],0,, +Positive maximum particle radius [m],2.5E-05,, +Positive area-weighted particle-size distribution [m-1],[function]lico2_lognormal_particle_size_distribution Positive electrode surface area to volume ratio [m-1],150000,Scott Moura FastDFN, Positive electrode Bruggeman coefficient (electrolyte),1.5,Scott Moura FastDFN, Positive electrode Bruggeman coefficient (electrode),1.5,Scott Moura FastDFN, diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 45b75da39b..671102d285 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -149,30 +149,31 @@ def set_standard_output_variables(self): } ) """ - #################### - # Overwrite defaults - #################### + @property def default_parameter_values(self): # Default parameter values default_params = super().default_parameter_values + # Extract the particle radius, taken to be the average radius R_n_dim = default_params["Negative particle radius [m]"] R_p_dim = default_params["Positive particle radius [m]"] # Additional particle distribution parameter values # Area-weighted standard deviations - sd_a_n = 0.5 + sd_a_n = 0.3 sd_a_p = 0.3 - sd_a_n_dim = sd_a_n * R_n_dim - sd_a_p_dim = sd_a_p * R_p_dim - # Max radius in the particle-size distribution (dimensionless). + # Minimum radius in the particle-size distributions (dimensionless). + R_min_n = 0 + R_min_p = 0 + + # Max radius in the particle-size distributions (dimensionless). # Either 5 s.d.'s above the mean or 2 times the mean, whichever is larger - R_n_max = max(2, 1 + sd_a_n * 5) - R_p_max = max(2, 1 + sd_a_p * 5) + R_max_n = max(2, 1 + sd_a_n * 5) + R_max_p = max(2, 1 + sd_a_p * 5) - # lognormal area-weighted particle-size distribution + # Define lognormal distribution def lognormal_distribution(R, R_av, sd): ''' A lognormal distribution with arguments @@ -193,28 +194,26 @@ def lognormal_distribution(R, R_av, sd): # Set the (area-weighted) particle-size distributions (dimensional) def f_a_dist_n_dim(R): - return lognormal_distribution(R, R_n_dim, sd_a_n_dim) + return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) def f_a_dist_p_dim(R): - return lognormal_distribution(R, R_p_dim, sd_a_p_dim) + return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) - # Update default parameters + # Append to default parameters (dimensional) default_params.update( { - "Negative area-weighted particle-size standard deviation": sd_a_n, "Negative area-weighted particle-size " - + "standard deviation [m]": sd_a_n_dim, - "Positive area-weighted particle-size standard deviation": sd_a_p, + + "standard deviation [m]": sd_a_n * R_n_dim, "Positive area-weighted particle-size " - + "standard deviation [m]": sd_a_p_dim, - "Negative maximum particle radius": R_n_max , - "Negative maximum particle radius [m]": R_n_max * R_n_dim, - "Positive maximum particle radius": R_p_max, - "Positive maximum particle radius [m]": R_p_max * R_p_dim, + + "standard deviation [m]": sd_a_p * R_p_dim, + "Negative minimum particle radius [m]": R_min_n * R_n_dim, + "Positive minimum particle radius [m]": R_min_p * R_p_dim, + "Negative maximum particle radius [m]": R_max_n * R_n_dim, + "Positive maximum particle radius [m]": R_max_p * R_p_dim, "Negative area-weighted " - + "particle-size distribution [m]": f_a_dist_n_dim, + + "particle-size distribution [m-1]": f_a_dist_n_dim, "Positive area-weighted " - + "particle-size distribution [m]": f_a_dist_p_dim, + + "particle-size distribution [m-1]": f_a_dist_p_dim, }, check_already_exists=False, ) diff --git a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py index bd6f5e529b..1b706cc098 100644 --- a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py @@ -45,7 +45,8 @@ def get_fundamental_variables(self): bounds=(0, 1), ) R = pybamm.standard_spatial_vars.R_variable_n - R_variable = pybamm.SecondaryBroadcast(R, ["negative electrode"]) + R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) + R_variable = pybamm.SecondaryBroadcast(R_variable, ["negative electrode"]) R_dim = self.param.R_n # Particle-size distribution (area-weighted) @@ -63,7 +64,8 @@ def get_fundamental_variables(self): bounds=(0, 1), ) R = pybamm.standard_spatial_vars.R_variable_p - R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) + R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) + R_variable = pybamm.SecondaryBroadcast(R_variable, ["positive electrode"]) R_dim = self.param.R_p # Particle-size distribution (area-weighted) @@ -106,7 +108,7 @@ def get_fundamental_variables(self): + " distribution": pybamm.x_average(f_a_dist), self.domain + " area-weighted particle-size" - + " distribution [m]": pybamm.x_average(f_a_dist) / R_dim, + + " distribution [m-1]": pybamm.x_average(f_a_dist) / R_dim, } ) return variables diff --git a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py index 9c68b2caaf..abf0e1c87d 100644 --- a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py @@ -97,7 +97,7 @@ def get_fundamental_variables(self): + " distribution": f_a_dist, self.domain + " area-weighted particle-size" - + " distribution [m]": f_a_dist / R_dim, + + " distribution [m-1]": f_a_dist / R_dim, } ) return variables diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py index d9405bd9fc..e310c1f723 100644 --- a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py @@ -82,7 +82,7 @@ def get_fundamental_variables(self): + " distribution": pybamm.x_average(f_a_dist), self.domain + " area-weighted particle-size" - + " distribution [m]": pybamm.x_average(f_a_dist) / R_dim, + + " distribution [m-1]": pybamm.x_average(f_a_dist) / R_dim, } ) return variables diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py index 165217f905..57805499d0 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -79,7 +79,7 @@ def get_fundamental_variables(self): self.domain + " area-weighted particle-size" + " distribution": f_a_dist, self.domain + " area-weighted particle-size" - + " distribution [m]": f_a_dist / R_dim, + + " distribution [m-1]": f_a_dist / R_dim, } ) return variables diff --git a/pybamm/parameters/geometric_parameters.py b/pybamm/parameters/geometric_parameters.py index 06226f82e0..a27a07ea73 100644 --- a/pybamm/parameters/geometric_parameters.py +++ b/pybamm/parameters/geometric_parameters.py @@ -44,6 +44,12 @@ b_s_n = pybamm.Parameter("Negative electrode Bruggeman coefficient (electrode)") b_s_s = pybamm.Parameter("Separator Bruggeman coefficient (electrode)") b_s_p = pybamm.Parameter("Positive electrode Bruggeman coefficient (electrode)") + +# Particle-size distribution geometry +R_min_n_dim = pybamm.Parameter("Negative minimum particle radius [m]") +R_min_p_dim = pybamm.Parameter("Positive minimum particle radius [m]") +R_max_n_dim = pybamm.Parameter("Negative maximum particle radius [m]") +R_max_p_dim = pybamm.Parameter("Positive maximum particle radius [m]") sd_a_n_dim = pybamm.Parameter( "Negative area-weighted particle-size standard deviation [m]" ) @@ -80,3 +86,9 @@ # Microscale geometry sd_a_n = sd_a_n_dim / R_n sd_a_p = sd_a_p_dim / R_p + +# Particle-size distribution geometry +R_min_n = R_min_n_dim / R_n +R_min_p = R_min_p_dim / R_p +R_max_n = R_max_n_dim / R_n +R_max_p = R_max_p_dim / R_p diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index ecba4aba8c..662cb87936 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -284,7 +284,7 @@ def f_a_dist_n_dimensional(R): "Negative particle-size variable [m]": R, } return pybamm.FunctionParameter( - "Negative area-weighted particle-size distribution [m]", inputs, + "Negative area-weighted particle-size distribution [m-1]", inputs, ) @@ -294,7 +294,7 @@ def f_a_dist_p_dimensional(R): "Positive particle-size variable [m]": R, } return pybamm.FunctionParameter( - "Positive area-weighted particle-size distribution [m]", inputs, + "Positive area-weighted particle-size distribution [m-1]", inputs, ) From 010ac48b3491a3a1f223f01fa0e84b78417015c4 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 18 Aug 2020 11:12:21 +0100 Subject: [PATCH 19/67] automatic distribution normalising --- pybamm/expression_tree/unary_operators.py | 3 ++- .../particle/fast_many_particle_size_distributions.py | 8 ++++++-- .../particle/fast_single_particle_size_distribution.py | 4 ++++ .../fickian_many_particle_size_distributions.py | 10 +++++++--- .../fickian_single_particle_size_distribution.py | 6 +++++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 7d5df79b00..b6e7ef74c1 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1235,7 +1235,8 @@ def R_average(symbol, domain): elif domain.lower() == "positive": f_a_dist = pybamm.standard_parameters_lithium_ion.f_a_dist_p(R) - return Integral(f_a_dist * symbol, R) + # enforce true average, normalising f_a_dist if it is not already + return Integral(f_a_dist * symbol, R) / Integral(f_a_dist, R) def boundary_value(symbol, side): diff --git a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py index 1b706cc098..ffb265f724 100644 --- a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py @@ -44,7 +44,7 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_n + R = pybamm.standard_spatial_vars.R_variable_n # used for averaging R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) R_variable = pybamm.SecondaryBroadcast(R_variable, ["negative electrode"]) R_dim = self.param.R_n @@ -63,7 +63,7 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_p + R = pybamm.standard_spatial_vars.R_variable_p # used for averaging R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) R_variable = pybamm.SecondaryBroadcast(R_variable, ["positive electrode"]) R_dim = self.param.R_p @@ -71,6 +71,10 @@ def get_fundamental_variables(self): # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) + # Ensure the distribution is normalised, irrespective of discretisation + # or user input + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R) + # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( 0, diff --git a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py index abf0e1c87d..ae80019b21 100644 --- a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py @@ -60,6 +60,10 @@ def get_fundamental_variables(self): # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) + # Ensure the distribution is normalised, irrespective of discretisation + # or user input + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) + # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( 0, diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py index e310c1f723..32247a7fde 100644 --- a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py @@ -39,7 +39,7 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_n + R = pybamm.standard_spatial_vars.R_variable_n # used for averaging R_variable = pybamm.SecondaryBroadcast(R, ["negative electrode"]) R_dim = self.param.R_n @@ -57,14 +57,18 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_p + R = pybamm.standard_spatial_vars.R_variable_p # used for averaging R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) R_dim = self.param.R_p # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) - # Standard R-averaged variables + # Ensure the distribution is normalised, irrespective of discretisation + # or user input + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R) + + # Standard R-averaged variables (avg secondary domain) c_s = pybamm.Integral(f_a_dist * c_s_distribution, R) c_s_xav = pybamm.x_average(c_s) variables = self._get_standard_concentration_variables(c_s, c_s_xav) diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py index 57805499d0..e439615072 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -61,7 +61,11 @@ def get_fundamental_variables(self): # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) - # Standard R-averaged variables + # Ensure the distribution is normalised, irrespective of discretisation + # or user input + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) + + # Standard R-averaged variables (avg secondary domain) c_s_xav = pybamm.Integral(f_a_dist * c_s_xav_distribution, R_variable) c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) variables = self._get_standard_concentration_variables(c_s, c_s_xav) From 45250578d4dfd0f86815f5b51992c9d29ea06b77 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 13 Nov 2020 16:57:24 +0000 Subject: [PATCH 20/67] changed event tolerance --- .../particle/fickian_many_particle_size_distributions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py index 32247a7fde..e269c564dd 100644 --- a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py @@ -231,7 +231,7 @@ def set_events(self, variables): c_s_surf_distribution = variables[ self.domain + " particle surface concentration distribution" ] - tol = 1e-4 + tol = 1e-5 self.events.append( pybamm.Event( From 11f118890455f8949adefff085003be5e2901eeb Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 1 Jun 2021 13:01:36 +0100 Subject: [PATCH 21/67] revert submodel selection changes to SPM --- pybamm/models/full_battery_models/lithium_ion/spm.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index e6dfb5dfb5..d4acc76039 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -69,10 +69,7 @@ def set_convection_submodel(self): def set_interfacial_submodel(self): - if ( - self.options["surface form"] is False - and self.options["particle-size distribution"] is False - ): + if self.options["surface form"] == "false": self.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( self.param, "Negative", "lithium-ion main", self.options ) From af4e016c9b34154884f03e6e0ebd6e0b7f5e47de Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 1 Jun 2021 13:50:52 +0100 Subject: [PATCH 22/67] fix SPM interface submodel selection --- pybamm/models/full_battery_models/lithium_ion/spm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index d4acc76039..e6dfb5dfb5 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -69,7 +69,10 @@ def set_convection_submodel(self): def set_interfacial_submodel(self): - if self.options["surface form"] == "false": + if ( + self.options["surface form"] is False + and self.options["particle-size distribution"] is False + ): self.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( self.param, "Negative", "lithium-ion main", self.options ) From 686c5e2d0814b6848141eeb1b481b43853630175 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 1 Jun 2021 15:33:54 +0100 Subject: [PATCH 23/67] fix tests --- .../interface/inverse_kinetics/inverse_butler_volmer.py | 5 ++++- .../models/submodels/interface/kinetics/base_kinetics.py | 8 ++++++-- .../test_full_battery_models/test_base_battery_model.py | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) 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 0f807389e5..84496ae367 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -30,7 +30,10 @@ class InverseButlerVolmer(BaseInterface): def __init__(self, param, domain, reaction, options=None): super().__init__(param, domain, reaction) if options is None: - options = {"sei film resistance": None} + options = { + "sei film resistance": None, + "particle-size distribution": False + } self.options = options def get_coupled_variables(self, variables): diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index d0949c2254..64063a6bef 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -20,7 +20,8 @@ class BaseKinetics(BaseInterface): The name of the reaction being implemented options: dict A dictionary of options to be passed to the model. In this case "sei film - resistance" is the important option. See :class:`pybamm.BaseBatteryModel` + resistance" and "particle-size distribution" are the important options. + See :class:`pybamm.BaseBatteryModel` **Extends:** :class:`pybamm.interface.BaseInterface` """ @@ -28,7 +29,10 @@ class BaseKinetics(BaseInterface): def __init__(self, param, domain, reaction, options=None): super().__init__(param, domain, reaction) if options is None: - options = {"sei film resistance": None} + options = { + "sei film resistance": None, + "particle-size distribution": False + } self.options = options def get_fundamental_variables(self): 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 c1c63b9a26..b8e1476d88 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 @@ -86,6 +86,8 @@ def test_default_var_pts(self): var.r_p: 30, var.y: 10, var.z: 10, + var.R_variable_n: 30, + var.R_variable_p: 30, } model = pybamm.BaseBatteryModel({"dimensionality": 0}) self.assertDictEqual(var_pts, model.default_var_pts) From 683f00830cd8927e44985076a512002935994219 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 2 Jun 2021 16:39:36 +0100 Subject: [PATCH 24/67] fixed MPM --- pybamm/expression_tree/unary_operators.py | 9 +- pybamm/geometry/battery_geometry.py | 21 +++-- .../full_battery_models/base_battery_model.py | 6 +- .../full_battery_models/lithium_ion/mpm.py | 84 +++---------------- .../submodels/interface/base_interface.py | 66 +++++++++------ ...ckian_single_particle_size_distribution.py | 61 ++++++++++---- 6 files changed, 119 insertions(+), 128 deletions(-) diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 157223f93c..aaaa68efe4 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1344,7 +1344,7 @@ def r_average(symbol): return Integral(symbol, r) / Integral(v, r) -def R_average(symbol, domain): +def R_average(symbol, domain, param): """convenience function for averaging over particle size R. Parameters @@ -1353,6 +1353,9 @@ def R_average(symbol, domain): The function to be averaged domain : str The electrode for averaging, either "negative" or "positive" + param : :class:`pybamm.LithiumIonParameters` + The parameter object containing the particle-size distributions. + Only implemented for the lithium-ion chemistry. Returns ------- :class:`Symbol` @@ -1388,9 +1391,9 @@ def R_average(symbol, domain): coord_sys="cartesian", ) if domain.lower() == "negative": - f_a_dist = pybamm.standard_parameters_lithium_ion.f_a_dist_n(R) + f_a_dist = param.f_a_dist_n(R) elif domain.lower() == "positive": - f_a_dist = pybamm.standard_parameters_lithium_ion.f_a_dist_p(R) + f_a_dist = param.f_a_dist_p(R) # enforce true average, normalising f_a_dist if it is not already return Integral(f_a_dist * symbol, R) / Integral(f_a_dist, R) diff --git a/pybamm/geometry/battery_geometry.py b/pybamm/geometry/battery_geometry.py index a6898ddd8b..228e5e93a0 100644 --- a/pybamm/geometry/battery_geometry.py +++ b/pybamm/geometry/battery_geometry.py @@ -6,7 +6,7 @@ def battery_geometry( include_particles=True, - particle_size_distribution=False, + options=None, current_collector_dimension=0, ): """ @@ -16,8 +16,9 @@ def battery_geometry( ---------- include_particles : bool Whether to include particle domains - particle_size_distribution : bool - Whether to include size domains for particle-size distributions + options : dict + Dictionary of model options. Necessary for "particle-size geometry", + relevant for lithium-ion chemistries. current_collector_dimensions : int, default The dimensions of the current collector. Should be 0 (default), 1 or 2 @@ -46,11 +47,15 @@ def battery_geometry( } ) # Add particle-size domains - if particle_size_distribution is True: - R_min_n = pybamm.geometric_parameters.R_min_n - R_min_p = pybamm.geometric_parameters.R_min_p - R_max_n = pybamm.geometric_parameters.R_max_n - R_max_p = pybamm.geometric_parameters.R_max_p + if ( + options is not None and + options["particle-size distribution"] == "true" + ): + param = pybamm.LithiumIonParameters(options) + R_min_n = param.R_min_n + R_min_p = param.R_min_p + R_max_n = param.R_max_n + R_max_p = param.R_max_p geometry.update( { "negative particle-size domain": { diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index e81fbc96cf..08be497a9a 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -69,8 +69,8 @@ class Options(pybamm.FuzzyDict): area per unit volume can be passed as a parameter, and is therefore not necessarily consistent with the particle shape. * "particle-size distribution" : str - Sets the model to include a single active particle size or a - distribution of sizes for each electrode. Can be "true" or + Sets the model to include a single active particle size or a + distribution of sizes for each electrode. Can be "true" or "false" (default). * "particle cracking" : str Sets the model to account for mechanical effects and particle @@ -354,7 +354,7 @@ def default_parameter_values(self): @property def default_geometry(self): return pybamm.battery_geometry( - particle_size_distribution=self.options["particle-size distribution"], + options=self.options, current_collector_dimension=self.options["dimensionality"] ) diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 4fd3493552..e9b14ef43c 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -40,6 +40,8 @@ def __init__( # Set submodels self.set_external_circuit_submodel() self.set_porosity_submodel() + self.set_crack_submodel() + self.set_active_material_submodel() self.set_tortuosity_submodels() self.set_convection_submodel() self.set_interfacial_submodel() @@ -50,7 +52,9 @@ def __init__( self.set_positive_electrode_submodel() self.set_thermal_submodel() self.set_current_collector_submodel() + self.set_sei_submodel() + self.set_lithium_plating_submodel() if build: self.build_model() @@ -165,7 +169,14 @@ def set_electrolyte_submodel(self): surf_form = pybamm.electrolyte_conductivity.surface_potential_form - if self.options["surface form"] is False: + if self.options["electrolyte conductivity"] not in ["default", "leading order"]: + raise pybamm.OptionError( + "electrolyte conductivity '{}' not suitable for SPM".format( + self.options["electrolyte conductivity"] + ) + ) + + if self.options["surface form"] == "false": self.submodels[ "leading-order electrolyte conductivity" ] = pybamm.electrolyte_conductivity.LeadingOrder(self.param) @@ -181,6 +192,7 @@ def set_electrolyte_submodel(self): self.submodels[ "leading-order " + domain.lower() + " electrolyte conductivity" ] = surf_form.LeadingOrderAlgebraic(self.param, domain) + self.submodels[ "electrolyte diffusion" ] = pybamm.electrolyte_diffusion.ConstantConcentration(self.param) @@ -272,73 +284,3 @@ def f_a_dist_p_dim(R): return default_params -''' - @property - def default_geometry(self): - default_geom = super().default_geometry - - # New Spatial Variables - R_variable_n = pybamm.standard_spatial_vars.R_variable_n - R_variable_p = pybamm.standard_spatial_vars.R_variable_p - - # append new domains - default_geom.update( - { - "negative particle-size domain": { - R_variable_n: { - "min": pybamm.Scalar(0), - "max": pybamm.Parameter("Negative maximum particle radius"), - } - }, - "positive particle-size domain": { - R_variable_p: { - "min": pybamm.Scalar(0), - "max": pybamm.Parameter("Positive maximum particle radius"), - } - }, - } - ) - return default_geom -''' -''' - @property - def default_var_pts(self): - defaults = super().default_var_pts - - # New Spatial Variables - R_variable_n = pybamm.standard_spatial_vars.R_variable_n - R_variable_p = pybamm.standard_spatial_vars.R_variable_p - # add to dictionary - defaults.update({R_variable_n: 50, R_variable_p: 50}) - return defaults -''' -''' - @property - def default_submesh_types(self): - default_submeshes = super().default_submesh_types - - default_submeshes.update( - { - "negative particle-size domain": pybamm.MeshGenerator( - pybamm.Uniform1DSubMesh - ), - "positive particle-size domain": pybamm.MeshGenerator( - pybamm.Uniform1DSubMesh - ), - } - ) - return default_submeshes -''' -''' - @property - def default_spatial_methods(self): - default_spatials = super().default_spatial_methods - - default_spatials.update( - { - "negative particle-size domain": pybamm.FiniteVolume(), - "positive particle-size domain": pybamm.FiniteVolume(), - } - ) - return default_spatials -''' diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 7fca83be70..089eb319ee 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -67,33 +67,47 @@ def _get_exchange_current_density(self, variables): c_s_surf = variables[ self.domain + " particle surface concentration distribution" ] + # If all variables were broadcast (in "x"), take only the orphans, + # then re-broadcast c_e + if ( + isinstance(c_s_surf, pybamm.Broadcast) + and isinstance(c_e, pybamm.Broadcast) + and isinstance(T, pybamm.Broadcast) + ): + c_s_surf = c_s_surf.orphans[0] + c_e = c_e.orphans[0] + T = T.orphans[0] + + # as c_e must now be a scalar, re-broadcast to + # "current collector" + c_e = pybamm.PrimaryBroadcast( + c_e, ["current collector"], + ) + # broadcast c_e, T onto "particle-size domain" + c_e = pybamm.PrimaryBroadcast( + c_e, [self.domain.lower() + " particle-size domain"] + ) + T = pybamm.PrimaryBroadcast( + T, [self.domain.lower() + " particle-size domain"] + ) + else: c_s_surf = variables[self.domain + " particle surface concentration"] - # If all variables were broadcast, take only the orphans - if ( - isinstance(c_s_surf, pybamm.Broadcast) - and isinstance(c_e, pybamm.Broadcast) - and isinstance(T, pybamm.Broadcast) - ): - c_s_surf = c_s_surf.orphans[0] - c_e = c_e.orphans[0] - T = T.orphans[0] + # If all variables were broadcast, take only the orphans + if ( + isinstance(c_s_surf, pybamm.Broadcast) + and isinstance(c_e, pybamm.Broadcast) + and isinstance(T, pybamm.Broadcast) + ): + c_s_surf = c_s_surf.orphans[0] + c_e = c_e.orphans[0] + T = T.orphans[0] tol = 1e-8 c_e = pybamm.maximum(tol, c_e) c_s_surf = pybamm.maximum(tol, pybamm.minimum(c_s_surf, 1 - tol)) - # For "particle-size distribution" submodels, broadcast c_e, T onto - # particle-size domain - if self.options["particle-size distribution"] == "true": - c_e = pybamm.PrimaryBroadcast( - c_e, [self.domain.lower() + " particle-size domain"] - ) - T = pybamm.PrimaryBroadcast( - T, [self.domain.lower() + " particle-size domain"] - ) - if self.domain == "Negative": j0 = self.param.j0_n(c_e, c_s_surf, T) / self.param.C_r_n elif self.domain == "Positive": @@ -429,7 +443,7 @@ def _get_standard_exchange_current_variables(self, j0): # output exchange current density if j0.domain == [self.domain.lower() + " particle-size domain"]: # R-average - j0 = pybamm.R_average(j0, self.domain) + j0 = pybamm.R_average(j0, self.domain, self.param) # X-average, and broadcast if necessary if j0.domain == []: @@ -516,7 +530,7 @@ def _get_standard_overpotential_variables(self, eta_r): # output reaction overpotential if eta_r.domain == [self.domain.lower() + " particle-size domain"]: # R-average - eta_r = pybamm.R_average(eta_r, self.domain) + eta_r = pybamm.R_average(eta_r, self.domain, self.param) # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) @@ -631,7 +645,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # output open circuit potential if ocp.domain == [self.domain.lower() + " particle-size domain"]: # R-average - ocp = pybamm.R_average(ocp, self.domain) + ocp = pybamm.R_average(ocp, self.domain, self.param) # X-average, and broadcast if necessary if ocp.domain == []: @@ -649,7 +663,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # output entropic change if dUdT.domain == [self.domain.lower() + " particle-size domain"]: # R-average - dUdT = pybamm.R_average(dUdT, self.domain) + dUdT = pybamm.R_average(dUdT, self.domain, self.param) dUdT_av = pybamm.x_average(dUdT) @@ -712,7 +726,7 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): j_distribution = self._get_kinetics(j0, ne, eta_r, T) # R-average - j = pybamm.R_average(j_distribution, self.domain) + j = pybamm.R_average(j_distribution, self.domain, self.param) return j, j_distribution def _get_standard_PSD_interfacial_current_variables(self, j_distribution): @@ -734,9 +748,9 @@ def _get_standard_PSD_interfacial_current_variables(self, j_distribution): i_typ = self.param.i_typ L_x = self.param.L_x if self.domain == "Negative": - j_scale = i_typ / (self.param.a_n_dim * L_x) + j_scale = i_typ / (self.param.a_n_typ * L_x) elif self.domain == "Positive": - j_scale = i_typ / (self.param.a_p_dim * L_x) + j_scale = i_typ / (self.param.a_p_typ * L_x) variables = { self.domain diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py index 62a09ce495..1fd870fc0a 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -38,8 +38,16 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R_variable = pybamm.standard_spatial_vars.R_variable_n - R_dim = self.param.R_n + # Since concentration does not depend on "x", need a particle-size + # spatial variable R with only "current collector" as secondary + # domain + R_variable = pybamm.SpatialVariable( + "R_n", + domain=["negative particle-size domain"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", + ) + R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_n(R_variable) @@ -55,8 +63,16 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R_variable = pybamm.standard_spatial_vars.R_variable_p - R_dim = self.param.R_p + # Since concentration does not depend on "x", need a particle-size + # spatial variable R with only "current collector" as secondary + # domain + R_variable = pybamm.SpatialVariable( + "R_p", + domain=["positive particle-size domain"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", + ) + R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) @@ -70,7 +86,7 @@ def get_fundamental_variables(self): c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) variables = self._get_standard_concentration_variables(c_s, c_s_xav) - # Standard distribution variables (R-dependent) + # Standard concentration distribution variables (R-dependent) variables.update( self._get_standard_concentration_distribution_variables( c_s_xav_distribution @@ -92,7 +108,7 @@ def get_coupled_variables(self, variables): c_s_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " particle concentration distribution" ] - R_variable = variables[self.domain + " particle size"] + R_spatial_variable = variables[self.domain + " particle size"] f_a_dist = variables[self.domain + " area-weighted particle-size distribution"] # broadcast to particle-size domain then again into particle @@ -101,19 +117,25 @@ def get_coupled_variables(self, variables): [self.domain.lower() + " particle-size domain"], ) T_k_xav = pybamm.PrimaryBroadcast(T_k_xav, [self.domain.lower() + " particle"],) + R = pybamm.PrimaryBroadcast( + R_spatial_variable, [self.domain.lower() + " particle"], + ) + f_a_dist = pybamm.PrimaryBroadcast( + f_a_dist, [self.domain.lower() + " particle"], + ) if self.domain == "Negative": N_s_xav_distribution = -self.param.D_n( c_s_xav_distribution, T_k_xav - ) * pybamm.grad(c_s_xav_distribution) / R_variable + ) * pybamm.grad(c_s_xav_distribution) / R elif self.domain == "Positive": N_s_xav_distribution = -self.param.D_p( c_s_xav_distribution, T_k_xav - ) * pybamm.grad(c_s_xav_distribution) / R_variable + ) * pybamm.grad(c_s_xav_distribution) / R # Standard R-averaged flux variables - N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_variable) - N_s = pybamm.SecondaryBroadcast(N_s_xav, [self._domain.lower() + " electrode"]) + N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_spatial_variable) + N_s = pybamm.SecondaryBroadcast(N_s_xav, [self.domain.lower() + " electrode"]) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) # Standard distribution flux variables (R-dependent) @@ -128,6 +150,7 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): + # Extract x-av variables c_s_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " particle concentration distribution" ] @@ -136,22 +159,26 @@ def set_rhs(self, variables): "X-averaged " + self.domain.lower() + " particle flux distribution" ] - R_variable = variables[self.domain + " particle size"] + # Spatial variable R, broadcast into particle + R_spatial_variable = variables[self.domain + " particle size"] + R = pybamm.PrimaryBroadcast( + R_spatial_variable, [self.domain.lower() + " particle"], + ) if self.domain == "Negative": self.rhs = { c_s_xav_distribution: -(1 / self.param.C_n) * pybamm.div(N_s_xav_distribution) - / R_variable + / R } elif self.domain == "Positive": self.rhs = { c_s_xav_distribution: -(1 / self.param.C_p) * pybamm.div(N_s_xav_distribution) - / R_variable + / R } def set_boundary_conditions(self, variables): - # Extract variables + # Extract x-av variables c_s_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " particle concentration distribution" ] @@ -165,7 +192,7 @@ def set_boundary_conditions(self, variables): + self.domain.lower() + " electrode interfacial current density distribution" ] - R_variable = variables[self.domain + " particle size"] + R = variables[self.domain + " particle size"] # Extract x-av T and broadcast to particle-size domain T_k_xav = variables[ @@ -179,7 +206,7 @@ def set_boundary_conditions(self, variables): if self.domain == "Negative": rbc = ( -self.param.C_n - * R_variable + * R * j_xav_distribution / self.param.a_R_n / self.param.D_n(c_s_surf_xav_distribution, T_k_xav) @@ -188,7 +215,7 @@ def set_boundary_conditions(self, variables): elif self.domain == "Positive": rbc = ( -self.param.C_p - * R_variable + * R * j_xav_distribution / self.param.a_R_p / self.param.gamma_p From 5784af90a0e12e8c974e8388eb4af00055b14b97 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 4 Jun 2021 11:15:30 +0100 Subject: [PATCH 25/67] name changes to domains, options, spatial vars --- .../plots_of_PSDModel.py | 135 ++++++++++++++++++ pybamm/expression_tree/broadcasts.py | 28 ++-- pybamm/expression_tree/unary_operators.py | 8 +- pybamm/geometry/battery_geometry.py | 12 +- pybamm/geometry/standard_spatial_vars.py | 16 +-- .../full_battery_models/base_battery_model.py | 22 +-- .../lithium_ion/base_lithium_ion_model.py | 4 +- .../full_battery_models/lithium_ion/dfn.py | 2 +- .../full_battery_models/lithium_ion/mpm.py | 19 +-- .../ohm/leading_size_distribution_ohm.py | 4 - .../submodels/interface/base_interface.py | 55 +++---- .../inverse_kinetics/inverse_butler_volmer.py | 2 +- .../interface/kinetics/base_kinetics.py | 8 +- .../submodels/particle/base_particle.py | 10 +- .../fast_many_particle_size_distributions.py | 20 +-- .../fast_single_particle_size_distribution.py | 12 +- ...ickian_many_particle_size_distributions.py | 95 ++++++++---- ...ckian_single_particle_size_distribution.py | 16 +-- pybamm/solvers/processed_variable.py | 12 +- .../test_base_battery_model.py | 4 +- 20 files changed, 322 insertions(+), 162 deletions(-) create mode 100644 add-PSD-scripts-and-notebooks/plots_of_PSDModel.py diff --git a/add-PSD-scripts-and-notebooks/plots_of_PSDModel.py b/add-PSD-scripts-and-notebooks/plots_of_PSDModel.py new file mode 100644 index 0000000000..47d687b15e --- /dev/null +++ b/add-PSD-scripts-and-notebooks/plots_of_PSDModel.py @@ -0,0 +1,135 @@ +import pybamm +import matplotlib.pyplot as plt + + +pybamm.set_logging_level("DEBUG") + +# Experiment +# (Use default 1C discharge from full) +t_eval = [0, 3600] + +# Parameter values +params = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019) +R_n_dim = params["Negative particle radius [m]"] +R_p_dim = params["Positive particle radius [m]"] + +# Add distribution parameters + +# Standard deviations +sd_a_n = 0.3 # dimensionless +sd_a_p = 0.3 +sd_a_n_dim = sd_a_n * R_n_dim # dimensional +sd_a_p_dim = sd_a_p * R_p_dim + +# Minimum and maximum particle sizes (dimensionaless) +R_min_n = 0 +R_min_p = 0 +R_max_n = max(2, 1 + sd_a_n * 5) +R_max_p = max(2, 1 + sd_a_p * 5) + + +def lognormal_distribution(R, R_av, sd): + ''' + A lognormal distribution with arguments + R : particle radius + R_av: mean particle radius + sd : standard deviation + ''' + import numpy as np + + # calculate usual lognormal parameters + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / R + ) + + +# Set the dimensional (area-weighted) particle-size distributions +# Note: the only argument must be the particle size R +def f_a_dist_n_dim(R): + return lognormal_distribution(R, R_n_dim, sd_a_n_dim) + + +def f_a_dist_p_dim(R): + return lognormal_distribution(R, R_p_dim, sd_a_p_dim) + + +# input distribution params (dimensional) +distribution_params = { + "Negative area-weighted particle-size " + + "standard deviation [m]": sd_a_n_dim, + "Positive area-weighted particle-size " + + "standard deviation [m]": sd_a_p_dim, + "Negative minimum particle radius [m]": R_min_n * R_n_dim, + "Positive minimum particle radius [m]": R_min_p * R_p_dim, + "Negative maximum particle radius [m]": R_max_n * R_n_dim, + "Positive maximum particle radius [m]": R_max_p * R_p_dim, + "Negative area-weighted " + + "particle-size distribution [m-1]": f_a_dist_n_dim, + "Positive area-weighted " + + "particle-size distribution [m-1]": f_a_dist_p_dim, +} +params.update(distribution_params, check_already_exists=False) + +# MPM +model_1 = pybamm.lithium_ion.MPM(name="MPM") # default params + +# DFN with PSD option +model_2 = pybamm.lithium_ion.DFN( + #options={"particle-size distribution": "true"}, + #name="MP-DFN" +) + +# DFN (no particle-size distributions) +model_3 = pybamm.lithium_ion.DFN(name="DFN") + +models = [model_1, model_2, model_3] + +sims=[] +for model in models: + sim = pybamm.Simulation( + model, + parameter_values=params, + solver=pybamm.CasadiSolver(mode="fast") + ) + sims.append(sim) + +# Reduce number of points in R +var = pybamm.standard_spatial_vars +sims[1].var_pts.update( + { + var.R_n: 20, + var.R_p: 20, + } +) + +# Solve +for sim in sims: + sim.solve(t_eval=t_eval) + + +# Plot +output_variables = [ + "Negative particle surface concentration", + "Positive particle surface concentration", + "X-averaged negative particle surface concentration distribution", + "X-averaged positive particle surface concentration distribution", +# "Negative particle surface concentration distribution", +# "Positive particle surface concentration distribution", + "Negative area-weighted particle-size distribution", + "Positive area-weighted particle-size distribution", + "Terminal voltage [V]", +] +# MPM +sims[0].plot(output_variables) +# MPM and MP-DFN +#pybamm.dynamic_plot([sims[0], sims[1]], output_variables=output_variables) +#pybamm.dynamic_plot(sims[1], output_variables=[ +# "Negative particle surface concentration distribution", +# "Positive particle surface concentration distribution", +#]) +# MPM, MP-DFN and DFN +pybamm.dynamic_plot(sims) diff --git a/pybamm/expression_tree/broadcasts.py b/pybamm/expression_tree/broadcasts.py index bfe730cd7a..552fa1470b 100644 --- a/pybamm/expression_tree/broadcasts.py +++ b/pybamm/expression_tree/broadcasts.py @@ -105,14 +105,14 @@ def check_and_set_domains( "negative electrode", "separator", "positive electrode", - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", "negative particle", "positive particle", ]: raise pybamm.DomainError( """Primary broadcast from current collector domain must be to electrode - or separator or particle or particle-size domains""" + or separator or particle or particle size domains""" ) elif ( child.domain[0] @@ -124,20 +124,20 @@ def check_and_set_domains( and broadcast_domain[0] not in [ "negative particle", "positive particle", - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", ] ): raise pybamm.DomainError( """Primary broadcast from electrode or separator must be to particle - or particle-size domains""" + or particle size domains""" ) elif child.domain[0] in [ - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", ] and broadcast_domain[0] not in ["negative particle", "positive particle"]: raise pybamm.DomainError( - """Primary broadcast from particle-size domain must be to particle + """Primary broadcast from particle size domain must be to particle domain""" ) elif child.domain[0] in ["negative particle", "positive particle"]: @@ -223,8 +223,8 @@ def check_and_set_domains( "negative particle", "positive particle", ] and broadcast_domain[0] not in [ - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", "negative electrode", "separator", "positive electrode", @@ -234,8 +234,8 @@ def check_and_set_domains( electrode or separator domains""" ) if child.domain[0] in [ - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", ] and broadcast_domain[0] not in [ "negative electrode", "separator", @@ -243,7 +243,7 @@ def check_and_set_domains( "current collector" ]: raise pybamm.DomainError( - """Secondary broadcast from particle-size domain must be to + """Secondary broadcast from particle size domain must be to electrode or separator or current collector domains""" ) elif ( diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index aaaa68efe4..2830c06d8b 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1194,13 +1194,13 @@ def x_average(symbol): # as this will be easier to identify for simplifications later on if ( symbol.domain == ["negative particle"] or - symbol.domain == ["negative particle-size domain"] + symbol.domain == ["negative particle size"] ): x = pybamm.standard_spatial_vars.x_n l = geo.l_n elif ( symbol.domain == ["positive particle"] or - symbol.domain == ["positive particle-size domain"] + symbol.domain == ["positive particle size"] ): x = pybamm.standard_spatial_vars.x_p l = geo.l_p @@ -1373,8 +1373,8 @@ def R_average(symbol, domain, param): ) if symbol.domain not in [ - ["negative particle-size domain"], - ["positive particle-size domain"], + ["negative particle size"], + ["positive particle size"], ]: raise pybamm.DomainError( """R-average only implemented for primary 'particle size' domains, diff --git a/pybamm/geometry/battery_geometry.py b/pybamm/geometry/battery_geometry.py index 228e5e93a0..521e3d30f9 100644 --- a/pybamm/geometry/battery_geometry.py +++ b/pybamm/geometry/battery_geometry.py @@ -46,10 +46,10 @@ def battery_geometry( "positive particle": {var.r_p: {"min": 0, "max": 1}}, } ) - # Add particle-size domains + # Add particle size domains if ( options is not None and - options["particle-size distribution"] == "true" + options["particle size"] == "distribution" ): param = pybamm.LithiumIonParameters(options) R_min_n = param.R_min_n @@ -58,11 +58,11 @@ def battery_geometry( R_max_p = param.R_max_p geometry.update( { - "negative particle-size domain": { - var.R_variable_n: {"min": R_min_n, "max": R_max_n} + "negative particle size": { + var.R_n: {"min": R_min_n, "max": R_max_n} }, - "positive particle-size domain": { - var.R_variable_p: {"min": R_min_p, "max": R_max_p} + "positive particle size": { + var.R_p: {"min": R_min_p, "max": R_max_p} }, } ) diff --git a/pybamm/geometry/standard_spatial_vars.py b/pybamm/geometry/standard_spatial_vars.py index d584fc2157..b2752246a7 100644 --- a/pybamm/geometry/standard_spatial_vars.py +++ b/pybamm/geometry/standard_spatial_vars.py @@ -50,18 +50,18 @@ coord_sys="spherical polar", ) -R_variable_n = pybamm.SpatialVariable( +R_n = pybamm.SpatialVariable( "R_n", - domain=["negative particle-size domain"], + domain=["negative particle size"], # auxiliary_domains={ # "secondary": "negative electrode", # "tertiary": "current collector", # }, coord_sys="cartesian", ) -R_variable_p = pybamm.SpatialVariable( +R_p = pybamm.SpatialVariable( "R_p", - domain=["positive particle-size domain"], + domain=["positive particle size"], # auxiliary_domains={ # "secondary": "positive electrode", # "tertiary": "current collector", @@ -121,13 +121,13 @@ coord_sys="spherical polar", ) -R_variable_n_edge = pybamm.SpatialVariableEdge( +R_n_edge = pybamm.SpatialVariableEdge( "R_n", - domain=["negative particle-size domain"], + domain=["negative particle size"], coord_sys="cartesian", ) -R_variable_p_edge = pybamm.SpatialVariableEdge( +R_p_edge = pybamm.SpatialVariableEdge( "R_p", - domain=["positive particle-size domain"], + domain=["positive particle size"], coord_sys="cartesian", ) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 08be497a9a..420802f97b 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -68,10 +68,10 @@ class Options(pybamm.FuzzyDict): (default), "user" or "no particles". For the "user" option the surface area per unit volume can be passed as a parameter, and is therefore not necessarily consistent with the particle shape. - * "particle-size distribution" : str + * "particle size" : str Sets the model to include a single active particle size or a - distribution of sizes for each electrode. Can be "true" or - "false" (default). + distribution of sizes at any macroscale location. Can be "single" + (default) or "distribution". Option applies to both electrodes. * "particle cracking" : str Sets the model to account for mechanical effects and particle cracking. Can be "none", "no cracking", "negative", "positive" or @@ -185,7 +185,7 @@ def __init__(self, extra_options): "quartic profile", ], "particle shape": ["spherical", "user", "no particles"], - "particle-size distribution": ["true", "false"], + "particle size": ["single", "distribution"], "electrolyte conductivity": [ "default", "full", @@ -206,7 +206,7 @@ def __init__(self, extra_options): "current collector": "uniform", "particle": "Fickian diffusion", "particle shape": "spherical", - "particle-size distribution": "false", + "particle size": "single", "electrolyte conductivity": "default", "thermal": "isothermal", "cell geometry": "none", @@ -369,8 +369,8 @@ def default_var_pts(self): var.r_p: 30, var.y: 10, var.z: 10, - var.R_variable_n: 30, - var.R_variable_p: 30, + var.R_n: 30, + var.R_p: 30, } # Reduce the default points for 2D current collectors if self.options["dimensionality"] == 2: @@ -385,10 +385,10 @@ def default_submesh_types(self): "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "negative particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "positive particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), - "negative particle-size domain": pybamm.MeshGenerator( + "negative particle size": pybamm.MeshGenerator( pybamm.Uniform1DSubMesh ), - "positive particle-size domain": pybamm.MeshGenerator( + "positive particle size": pybamm.MeshGenerator( pybamm.Uniform1DSubMesh ), } @@ -410,8 +410,8 @@ def default_spatial_methods(self): "macroscale": pybamm.FiniteVolume(), "negative particle": pybamm.FiniteVolume(), "positive particle": pybamm.FiniteVolume(), - "negative particle-size domain": pybamm.FiniteVolume(), - "positive particle-size domain": pybamm.FiniteVolume(), + "negative particle size": pybamm.FiniteVolume(), + "positive particle size": pybamm.FiniteVolume(), } if self.options["dimensionality"] == 0: # 0D submesh - use base spatial method diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index ca93f90f4c..3468c8a517 100644 --- a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -27,8 +27,8 @@ def __init__(self, options=None, name="Unnamed lithium-ion model", build=False): "positive electrode": self.param.L_x, "negative particle": self.param.R_n_typ, "positive particle": self.param.R_p_typ, - "negative particle-size domain": self.param.R_n_typ, - "positive particle-size domain": self.param.R_p_typ, + "negative particle size": self.param.R_n_typ, + "positive particle size": self.param.R_p_typ, "current collector y": self.param.L_z, "current collector z": self.param.L_z, } diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index 8e47e83325..46af4a45b2 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -120,7 +120,7 @@ def set_interfacial_submodel(self): def set_particle_submodel(self): - if self.options["particle-size distribution"] == "true": + if self.options["particle size"] == "distribution": if self.options["particle"] == "Fickian diffusion": self.submodels["negative particle"] = pybamm.particle.FickianManyPSDs( self.param, "Negative" diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index e9b14ef43c..9f86df342f 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -35,7 +35,7 @@ def __init__( self, options=None, name="Many-Particle Model", build=True ): super().__init__(options, name) - self.options["particle-size distribution"] = "true" + self.options["particle size"] = "distribution" # Set submodels self.set_external_circuit_submodel() @@ -196,23 +196,6 @@ def set_electrolyte_submodel(self): self.submodels[ "electrolyte diffusion" ] = pybamm.electrolyte_diffusion.ConstantConcentration(self.param) - """ - def set_standard_output_variables(self): - super().set_standard_output_variables() - - # add particle-size variables - var = pybamm.standard_spatial_vars - R_n = pybamm.geometric_parameters.R_n - R_p = pybamm.geometric_parameters.R_p - self.variables.update( - { - "Negative particle size": var.R_variable_n, - "Negative particle size [m]": var.R_variable_n * R_n, - "Positive particle size": var.R_variable_p, - "Positive particle size [m]": var.R_variable_p * R_p, - } - ) - """ @property def default_parameter_values(self): diff --git a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py index 6879289738..0083fac88d 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py @@ -135,10 +135,6 @@ def _get_standard_surface_potential_difference_variables(self, delta_phi): else: delta_phi_av = pybamm.x_average(delta_phi) - # # For particle-size distributions (true here), must broadcast further - # delta_phi = pybamm.PrimaryBroadcast(delta_phi, [self.domain.lower() + " particle-size domain"]) - # delta_phi_av = pybamm.PrimaryBroadcast(delta_phi_av, [self.domain.lower() + " particle-size domain"]) - variables = { self.domain + " electrode surface potential difference": delta_phi, "X-averaged " diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 089eb319ee..7b7e50c3b7 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -63,7 +63,7 @@ def _get_exchange_current_density(self, variables): if self.reaction == "lithium-ion main": # For "particle-size distribution" submodels, take distribution version # of c_s_surf that depends on particle size. - if self.options["particle-size distribution"] == "true": + if self.options["particle size"] == "distribution": c_s_surf = variables[ self.domain + " particle surface concentration distribution" ] @@ -83,12 +83,12 @@ def _get_exchange_current_density(self, variables): c_e = pybamm.PrimaryBroadcast( c_e, ["current collector"], ) - # broadcast c_e, T onto "particle-size domain" + # broadcast c_e, T onto "particle size" c_e = pybamm.PrimaryBroadcast( - c_e, [self.domain.lower() + " particle-size domain"] + c_e, [self.domain.lower() + " particle size"] ) T = pybamm.PrimaryBroadcast( - T, [self.domain.lower() + " particle-size domain"] + T, [self.domain.lower() + " particle size"] ) else: @@ -163,26 +163,31 @@ def _get_open_circuit_potential(self, variables): T = variables[self.domain + " electrode temperature"] # For "particle-size distribution" models, take distribution version # of c_s_surf that depends on particle size. - if self.options["particle-size distribution"] == "true": + if self.options["particle size"] == "distribution": c_s_surf = variables[ self.domain + " particle surface concentration distribution" ] + # If variable was broadcast, take only the orphan + if ( + isinstance(c_s_surf, pybamm.Broadcast) + and isinstance(T, pybamm.Broadcast) + ): + c_s_surf = c_s_surf.orphans[0] + T = T.orphans[0] + T = pybamm.PrimaryBroadcast( + T, [self.domain.lower() + " particle size"] + ) else: c_s_surf = variables[self.domain + " particle surface concentration"] - # If variable was broadcast, take only the orphan - if ( - isinstance(c_s_surf, pybamm.Broadcast) - and isinstance(T, pybamm.Broadcast) - ): - c_s_surf = c_s_surf.orphans[0] - T = T.orphans[0] - # For "particle-size distribution" models, then broadcast T - # onto particle-size domain - if self.options["particle-size distribution"] == "true": - T = pybamm.PrimaryBroadcast( - T, [self.domain.lower() + " particle-size domain"] - ) + # If variable was broadcast, take only the orphan + if ( + isinstance(c_s_surf, pybamm.Broadcast) + and isinstance(T, pybamm.Broadcast) + ): + c_s_surf = c_s_surf.orphans[0] + T = T.orphans[0] + if self.domain == "Negative": ocp = self.param.U_n(c_s_surf, T) dUdT = self.param.dUdT_n(c_s_surf) @@ -441,7 +446,7 @@ def _get_standard_exchange_current_variables(self, j0): # If j0 depends on particle size R then must R-average to get standard # output exchange current density - if j0.domain == [self.domain.lower() + " particle-size domain"]: + if j0.domain == [self.domain.lower() + " particle size"]: # R-average j0 = pybamm.R_average(j0, self.domain, self.param) @@ -528,7 +533,7 @@ def _get_standard_overpotential_variables(self, eta_r): pot_scale = self.param.potential_scale # If eta_r depends on particle size R then must R-average to get standard # output reaction overpotential - if eta_r.domain == [self.domain.lower() + " particle-size domain"]: + if eta_r.domain == [self.domain.lower() + " particle size"]: # R-average eta_r = pybamm.R_average(eta_r, self.domain, self.param) @@ -643,7 +648,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): """ # If ocp depends on particle size R then must R-average to get standard # output open circuit potential - if ocp.domain == [self.domain.lower() + " particle-size domain"]: + if ocp.domain == [self.domain.lower() + " particle size"]: # R-average ocp = pybamm.R_average(ocp, self.domain, self.param) @@ -661,7 +666,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # If dUdT depends on particle size R then must R-average to get standard # output entropic change - if dUdT.domain == [self.domain.lower() + " particle-size domain"]: + if dUdT.domain == [self.domain.lower() + " particle size"]: # R-average dUdT = pybamm.R_average(dUdT, self.domain, self.param) @@ -717,9 +722,9 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): if eta_r.domains["secondary"] != [self.domain.lower() + " electrode"]: T = pybamm.x_average(T) - # Broadcast T onto "particle-size domain" + # Broadcast T onto "particle size" domain T = pybamm.PrimaryBroadcast( - T, [self.domain.lower() + " particle-size domain"] + T, [self.domain.lower() + " particle size"] ) # current density that depends on particle size R @@ -732,7 +737,7 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): def _get_standard_PSD_interfacial_current_variables(self, j_distribution): """ Interfacial current density variables that depend on particle size R, - relevant if "particle-size distribution" option is "true". + relevant if "particle size" option is "distribution". """ # X-average and broadcast if necessary if j_distribution.domains["secondary"] == [self.domain.lower() + " electrode"]: 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 bf0badc52c..be25244b99 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -32,7 +32,7 @@ def __init__(self, param, domain, reaction, options=None): if options is None: options = { "SEI film resistance": "none", - "particle-size distribution": False + "particle size": "distribution" } self.options = options diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index f2f7c3d97d..0fb23822e2 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -60,13 +60,13 @@ def get_coupled_variables(self, variables): if isinstance(delta_phi, pybamm.Broadcast): delta_phi = delta_phi.orphans[0] # For "particle-size distribution" models, delta_phi must then be - # broadcast to "particle-size domain" + # broadcast to "particle size" domain if ( self.reaction == "lithium-ion main" - and self.options["particle-size distribution"] == "true" + and self.options["particle size"] == "distribution" ): delta_phi = pybamm.PrimaryBroadcast( - delta_phi, [self.domain.lower() + " particle-size domain"] + delta_phi, [self.domain.lower() + " particle size"] ) # Get exchange-current density @@ -120,7 +120,7 @@ def get_coupled_variables(self, variables): # (In the "distributed SEI resistance" model, we have already defined j) if ( self.reaction == "lithium-ion main" - and self.options["particle-size distribution"] == "true" + and self.options["particle size"] == "distribution" ): # For "particle-size distribution" models, additional steps (R-averaging) # are necessary to calculate j diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index c5a60d63d9..772a4229f4 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -159,7 +159,7 @@ def _get_standard_concentration_distribution_variables(self, c_s): # Broadcast and x-average when necessary if c_s.domain == [ - self.domain.lower() + " particle-size domain" + self.domain.lower() + " particle size" ] and c_s.auxiliary_domains["secondary"] != [ self.domain.lower() + " electrode" ]: @@ -171,7 +171,7 @@ def _get_standard_concentration_distribution_variables(self, c_s): 0, [self.domain.lower() + " particle"], { - "secondary": self.domain.lower() + " particle-size domain", + "secondary": self.domain.lower() + " particle size", "tertiary": self.domain.lower() + " electrode", }, ) @@ -190,7 +190,7 @@ def _get_standard_concentration_distribution_variables(self, c_s): 0, [self.domain.lower() + " particle"], { - "secondary": self.domain.lower() + " particle-size domain", + "secondary": self.domain.lower() + " particle size", "tertiary": self.domain.lower() + " electrode", }, ) @@ -201,7 +201,7 @@ def _get_standard_concentration_distribution_variables(self, c_s): c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] ) elif c_s.domain == [ - self.domain.lower() + " particle-size domain" + self.domain.lower() + " particle size" ] and c_s.auxiliary_domains["secondary"] == [ self.domain.lower() + " electrode" ]: @@ -216,7 +216,7 @@ def _get_standard_concentration_distribution_variables(self, c_s): 0, [self.domain.lower() + " particle"], { - "secondary": self.domain.lower() + " particle-size domain", + "secondary": self.domain.lower() + " particle size", "tertiary": self.domain.lower() + " electrode", }, ) diff --git a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py index 36fb458207..74d0867250 100644 --- a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py @@ -37,17 +37,17 @@ def get_fundamental_variables(self): # distribution variables c_s_surf_distribution = pybamm.Variable( "Negative particle surface concentration distribution", - domain="negative particle-size domain", + domain="negative particle size", auxiliary_domains={ "secondary": "negative electrode", "tertiary": "current collector", }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_n # used for averaging + R = pybamm.standard_spatial_vars.R_n # used for averaging R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) R_variable = pybamm.SecondaryBroadcast(R_variable, ["negative electrode"]) - R_dim = self.param.R_n + R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_n(R_variable) @@ -56,17 +56,17 @@ def get_fundamental_variables(self): # distribution variables c_s_surf_distribution = pybamm.Variable( "Positive particle surface concentration distribution", - domain="positive particle-size domain", + domain="positive particle size", auxiliary_domains={ "secondary": "positive electrode", "tertiary": "current collector", }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_p # used for averaging + R = pybamm.standard_spatial_vars.R_p # used for averaging R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) R_variable = pybamm.SecondaryBroadcast(R_variable, ["positive electrode"]) - R_dim = self.param.R_p + R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) @@ -152,16 +152,16 @@ def set_initial_conditions(self, variables): ] if self.domain == "Negative": - # Broadcast x_n to particle-size domain + # Broadcast x_n to particle size domain x_n = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_n, "negative particle-size domain" + pybamm.standard_spatial_vars.x_n, "negative particle size" ) c_init = self.param.c_n_init(x_n) elif self.domain == "Positive": - # Broadcast x_p to particle-size domain + # Broadcast x_p to particle size domain x_p = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_p, "positive particle-size domain" + pybamm.standard_spatial_vars.x_p, "positive particle size" ) c_init = self.param.c_p_init(x_p) diff --git a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py index a77bb87f66..0a461449ff 100644 --- a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py @@ -36,12 +36,12 @@ def get_fundamental_variables(self): # distribution variables c_s_surf_xav_distribution = pybamm.Variable( "X-averaged negative particle surface concentration distribution", - domain="negative particle-size domain", + domain="negative particle size", auxiliary_domains={"secondary": "current collector"}, bounds=(0, 1), ) - R_variable = pybamm.standard_spatial_vars.R_variable_n - R_dim = self.param.R_n + R_variable = pybamm.standard_spatial_vars.R_n + R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_n(R_variable) @@ -50,12 +50,12 @@ def get_fundamental_variables(self): # distribution variables c_s_surf_xav_distribution = pybamm.Variable( "X-averaged positive particle surface concentration distribution", - domain="positive particle-size domain", + domain="positive particle size", auxiliary_domains={"secondary": "current collector"}, bounds=(0, 1), ) - R_variable = pybamm.standard_spatial_vars.R_variable_p - R_dim = self.param.R_p + R_variable = pybamm.standard_spatial_vars.R_p + R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py index 08771611ce..2523197276 100644 --- a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py @@ -34,14 +34,24 @@ def get_fundamental_variables(self): "Negative particle concentration distribution", domain="negative particle", auxiliary_domains={ - "secondary": "negative particle-size domain", + "secondary": "negative particle size", "tertiary": "negative electrode", }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_n # used for averaging - R_variable = pybamm.SecondaryBroadcast(R, ["negative electrode"]) - R_dim = self.param.R_n + # Since concentration does not depend on "y,z", need a particle-size + # spatial variable R with only "electrode" as secondary + # domain + R_variable = pybamm.SpatialVariable( + "R_n", + domain=["negative particle size"], + auxiliary_domains={ + "secondary": "negative electrode", + }, + coord_sys="cartesian", + ) + #R_variable = pybamm.standard_spatial_vars.R_n + R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_n(R_variable) @@ -52,24 +62,35 @@ def get_fundamental_variables(self): "Positive particle concentration distribution", domain="positive particle", auxiliary_domains={ - "secondary": "positive particle-size domain", + "secondary": "positive particle size", "tertiary": "positive electrode", }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_variable_p # used for averaging - R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) - R_dim = self.param.R_p + # Since concentration does not depend on "y,z", need a + # spatial variable R with only "electrode" as secondary + # domain + R_variable = pybamm.SpatialVariable( + "R_p", + domain=["positive particle size"], + auxiliary_domains={ + "secondary": "positive electrode", + }, + coord_sys="cartesian", + ) + #R = pybamm.standard_spatial_vars.R_p # used for averaging + #R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) + R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_variable) # Ensure the distribution is normalised, irrespective of discretisation # or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R) + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) # Standard R-averaged variables (avg secondary domain) - c_s = pybamm.Integral(f_a_dist * c_s_distribution, R) + c_s = pybamm.Integral(f_a_dist * c_s_distribution, R_variable) c_s_xav = pybamm.x_average(c_s) variables = self._get_standard_concentration_variables(c_s, c_s_xav) @@ -96,39 +117,57 @@ def get_coupled_variables(self, variables): self.domain + " particle concentration distribution" ] R_variable = variables[self.domain + " particle size"] + R = pybamm.PrimaryBroadcast(R_variable, [self.domain.lower() + " particle"],) + T_k = variables[self.domain + " electrode temperature"] - # broadcast to particle-size domain then again into particle - T_k = pybamm.PrimaryBroadcast( - variables[self.domain + " electrode temperature"], - [self.domain.lower() + " particle-size domain"], - ) - T_k = pybamm.PrimaryBroadcast(T_k, [self.domain.lower() + " particle"],) + # Variables can currently only have 3 domains, so remove "current collector" + # from T_k. If T_k was broadcast to "electrode", take orphan, average + # over "current collector", then broadcast to "particle", "particle-size" + # and "electrode" + if isinstance(T_k, pybamm.Broadcast): + T_k = pybamm.yz_average(T_k.orphans[0]) + T_k = pybamm.FullBroadcast( + T_k, self.domain.lower() + " particle", + { + "secondary": self.domain.lower() + " particle size", + "tertiary": self.domain.lower() + " electrode" + } + ) + else: + # broadcast to "particle size" domain then again into "particle" + T_k = pybamm.PrimaryBroadcast( + T_k, + [self.domain.lower() + " particle size"], + ) + T_k = pybamm.PrimaryBroadcast( + T_k, [self.domain.lower() + " particle"], + ) if self.domain == "Negative": N_s_distribution = ( -self.param.D_n(c_s_distribution, T_k) * pybamm.grad(c_s_distribution) - / R_variable + / R ) f_a_dist = self.param.f_a_dist_n(R_variable) # spatial var to use in R integral below (cannot use R_variable as # it is a broadcast) - R = pybamm.standard_spatial_vars.R_variable_n + #R = pybamm.standard_spatial_vars.R_n elif self.domain == "Positive": N_s_distribution = ( -self.param.D_p(c_s_distribution, T_k) * pybamm.grad(c_s_distribution) - / R_variable + / R ) f_a_dist = self.param.f_a_dist_p(R_variable) # spatial var to use in R integral below (cannot use R_variable as # it is a broadcast) - R = pybamm.standard_spatial_vars.R_variable_p + #R = pybamm.standard_spatial_vars.R_p # Standard R-averaged flux variables - N_s = pybamm.Integral(f_a_dist * N_s_distribution, R) + N_s = pybamm.Integral(f_a_dist * N_s_distribution, R_variable) variables.update(self._get_standard_flux_variables(N_s, N_s)) # Standard distribution flux variables (R-dependent) @@ -145,17 +184,19 @@ def set_rhs(self, variables): N_s_distribution = variables[self.domain + " particle flux distribution"] R_variable = variables[self.domain + " particle size"] + R = pybamm.PrimaryBroadcast(R_variable, [self.domain.lower() + " particle"],) + if self.domain == "Negative": self.rhs = { c_s_distribution: -(1 / self.param.C_n) * pybamm.div(N_s_distribution) - / R_variable + / R } elif self.domain == "Positive": self.rhs = { c_s_distribution: -(1 / self.param.C_p) * pybamm.div(N_s_distribution) - / R_variable + / R } def set_boundary_conditions(self, variables): @@ -171,10 +212,10 @@ def set_boundary_conditions(self, variables): ] R_variable = variables[self.domain + " particle size"] - # Extract T and broadcast to particle-size domain + # Extract T and broadcast to particle size domain T_k = variables[self.domain + " electrode temperature"] T_k = pybamm.PrimaryBroadcast( - T_k, [self.domain.lower() + " particle-size domain"] + T_k, [self.domain.lower() + " particle size"] ) # Set surface Neumann boundary values @@ -212,7 +253,7 @@ def set_initial_conditions(self, variables): if self.domain == "Negative": # Broadcast x_n to particle-size then into the particles x_n = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_n, "negative particle-size domain" + pybamm.standard_spatial_vars.x_n, "negative particle size" ) x_n = pybamm.PrimaryBroadcast(x_n, "negative particle") c_init = self.param.c_n_init(x_n) @@ -220,7 +261,7 @@ def set_initial_conditions(self, variables): elif self.domain == "Positive": # Broadcast x_n to particle-size then into the particles x_p = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_p, "positive particle-size domain" + pybamm.standard_spatial_vars.x_p, "positive particle size" ) x_p = pybamm.PrimaryBroadcast(x_p, "positive particle") c_init = self.param.c_p_init(x_p) diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py index 1fd870fc0a..d50e4729f2 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py @@ -33,7 +33,7 @@ def get_fundamental_variables(self): "X-averaged negative particle concentration distribution", domain="negative particle", auxiliary_domains={ - "secondary": "negative particle-size domain", + "secondary": "negative particle size", "tertiary": "current collector", }, bounds=(0, 1), @@ -43,7 +43,7 @@ def get_fundamental_variables(self): # domain R_variable = pybamm.SpatialVariable( "R_n", - domain=["negative particle-size domain"], + domain=["negative particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) @@ -58,7 +58,7 @@ def get_fundamental_variables(self): "X-averaged positive particle concentration distribution", domain="positive particle", auxiliary_domains={ - "secondary": "positive particle-size domain", + "secondary": "positive particle size", "tertiary": "current collector", }, bounds=(0, 1), @@ -68,7 +68,7 @@ def get_fundamental_variables(self): # domain R_variable = pybamm.SpatialVariable( "R_p", - domain=["positive particle-size domain"], + domain=["positive particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) @@ -111,10 +111,10 @@ def get_coupled_variables(self, variables): R_spatial_variable = variables[self.domain + " particle size"] f_a_dist = variables[self.domain + " area-weighted particle-size distribution"] - # broadcast to particle-size domain then again into particle + # broadcast to "particle size" domain then again into "particle" T_k_xav = pybamm.PrimaryBroadcast( variables["X-averaged " + self.domain.lower() + " electrode temperature"], - [self.domain.lower() + " particle-size domain"], + [self.domain.lower() + " particle size"], ) T_k_xav = pybamm.PrimaryBroadcast(T_k_xav, [self.domain.lower() + " particle"],) R = pybamm.PrimaryBroadcast( @@ -194,12 +194,12 @@ def set_boundary_conditions(self, variables): ] R = variables[self.domain + " particle size"] - # Extract x-av T and broadcast to particle-size domain + # Extract x-av T and broadcast to particle size domain T_k_xav = variables[ "X-averaged " + self.domain.lower() + " electrode temperature" ] T_k_xav = pybamm.PrimaryBroadcast( - T_k_xav, [self.domain.lower() + " particle-size domain"] + T_k_xav, [self.domain.lower() + " particle size"] ) # Set surface Neumann boundary values diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 21d7c5edb3..41a387f24c 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -177,8 +177,8 @@ def initialise_1D(self, fixed_t=False): self.first_dimension = "z" self.z_sol = space elif self.domain[0] in [ - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", ]: self.first_dimension = "R" self.R_sol = space @@ -325,16 +325,16 @@ def initialise_2D(self): "negative particle", "positive particle", ] and self.auxiliary_domains["secondary"][0] in [ - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", ]: self.first_dimension = "r" self.second_dimension = "R" self.r_sol = first_dim_pts self.R_sol = second_dim_pts elif self.domain[0] in [ - "negative particle-size domain", - "positive particle-size domain", + "negative particle size", + "positive particle size", ] and self.auxiliary_domains["secondary"][0] in [ "negative electrode", "positive electrode", 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 1ed27f2b68..e890d23c9e 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 @@ -120,8 +120,8 @@ def test_default_var_pts(self): var.r_p: 30, var.y: 10, var.z: 10, - var.R_variable_n: 30, - var.R_variable_p: 30, + var.R_n: 30, + var.R_p: 30, } model = pybamm.BaseBatteryModel({"dimensionality": 0}) self.assertDictEqual(var_pts, model.default_var_pts) From 65d568fc8b0a83c820ee027f18a403cb13eddd1e Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 4 Jun 2021 11:55:22 +0100 Subject: [PATCH 26/67] refactor of size distribution submodels --- .../lithium_ion/basic_mpm.py | 538 ------------------ .../full_battery_models/lithium_ion/dfn.py | 10 +- .../full_battery_models/lithium_ion/mpm.py | 12 +- pybamm/models/submodels/particle/__init__.py | 5 +- .../submodels/particle/base_particle.py | 114 ---- .../particle/size_distribution/__init__.py | 5 + .../size_distribution/base_distribution.py | 139 +++++ .../fast_many_distributions.py} | 6 +- .../fast_single_distribution.py} | 8 +- .../fickian_many_distributions.py} | 8 +- .../fickian_single_distribution.py} | 8 +- 11 files changed, 173 insertions(+), 680 deletions(-) delete mode 100644 pybamm/models/full_battery_models/lithium_ion/basic_mpm.py create mode 100644 pybamm/models/submodels/particle/size_distribution/__init__.py create mode 100644 pybamm/models/submodels/particle/size_distribution/base_distribution.py rename pybamm/models/submodels/particle/{fast_many_particle_size_distributions.py => size_distribution/fast_many_distributions.py} (97%) rename pybamm/models/submodels/particle/{fast_single_particle_size_distribution.py => size_distribution/fast_single_distribution.py} (96%) rename pybamm/models/submodels/particle/{fickian_many_particle_size_distributions.py => size_distribution/fickian_many_distributions.py} (97%) rename pybamm/models/submodels/particle/{fickian_single_particle_size_distribution.py => size_distribution/fickian_single_distribution.py} (97%) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py b/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py deleted file mode 100644 index 8132530aab..0000000000 --- a/pybamm/models/full_battery_models/lithium_ion/basic_mpm.py +++ /dev/null @@ -1,538 +0,0 @@ -# -# Basic Many Particle Model (MPM) -# -import pybamm -from .base_lithium_ion_model import BaseModel - - -class BasicMPM(BaseModel): - """Particle-Size Distribution (PSD) model of a lithium-ion battery, from [1]_. - - This class is similar to the :class:`pybamm.lithium_ion.SPM` model class in that it - shows the whole model in a single class. - - Parameters - ---------- - name : str, optional - The name of the model. - - References - ---------- - .. [1] TL Kirk, J Evans, CP Please and SJ Chapman. “Modelling electrode heterogeneity - in lithium-ion batteries: unimodal and bimodal particle-size distributions”. - In: arXiv preprint arXiv:2006.12208 (2020). - - - **Extends:** :class:`pybamm.lithium_ion.BaseModel` - """ - - def __init__(self, name="Particle-Size Distribution Model"): - super().__init__({}, name) - ###################### - # Parameters - ###################### - # Import all the standard parameters from base_lithium_ion_model.BaseModel - # (in turn from pybamm.standard_parameters_lithium_ion) - param = self.param - - # Additional parameters for this model - # Dimensionless standard deviations - sd_a_n = pybamm.Parameter( - "negative area-weighted particle-size standard deviation" - ) - sd_a_p = pybamm.Parameter( - "positive area-weighted particle-size standard deviation" - ) - - # Particle-size distributions (area-weighted) - def f_a_dist_n(R, R_av_a, sd_a): - inputs = { - "negative particle-size variable": R, - "negative area-weighted mean particle size": R_av_a, - "negative area-weighted particle-size standard deviation": sd_a, - } - return pybamm.FunctionParameter( - "negative area-weighted particle-size distribution", inputs, - ) - - def f_a_dist_p(R, R_av_a, sd_a): - inputs = { - "positive particle-size variable": R, - "positive area-weighted mean particle size": R_av_a, - "positive area-weighted particle-size standard deviation": sd_a, - } - return pybamm.FunctionParameter( - "positive area-weighted particle-size distribution", inputs, - ) - - # Set length scales for additional domains (particle-size domains) - self.length_scales.update( - { - "negative particle-size domain": param.R_n, - "positive particle-size domain": param.R_p, - } - ) - - ###################### - # Variables - ###################### - # Discharge capacity - Q = pybamm.Variable("Discharge capacity [A.h]") - # X-averaged particle concentrations: these now depend continuously on particle - # size with secondary domains "negative/positive particle-size domain" - c_s_n = pybamm.Variable( - "X-averaged negative particle concentration", - domain="negative particle", - auxiliary_domains={ - "secondary": "negative particle-size domain", - # "tertiary": "negative electrode", - }, - ) - c_s_p = pybamm.Variable( - "X-averaged positive particle concentration", - domain="positive particle", - auxiliary_domains={ - "secondary": "positive particle-size domain", - # "tertiary": "positive electrode" - }, - ) - # Electrode potentials (leave them without a domain for now) - phi_e = pybamm.Variable("Electrolyte potential") - phi_s_p = pybamm.Variable( - "Positive electrode potential" - # domain="positive particle-size domain", - # auxiliary_domains={ - # "secondary": "positive electrode", - # } - ) - - # Spatial Variables - R_variable_n = pybamm.SpatialVariable( - "negative particle-size variable", - domain=["negative particle-size domain"], # could add auxiliary domains - coord_sys="cartesian", - ) - R_variable_p = pybamm.SpatialVariable( - "positive particle-size variable", - domain=["positive particle-size domain"], # could add auxiliary domains - coord_sys="cartesian", - ) - - # Constant temperature - T = param.T_init - - ###################### - # Other set-up - ###################### - - # Current density - i_cell = param.current_with_time - - ###################### - # State of Charge - ###################### - I_dim = param.dimensional_current_with_time - # The `rhs` dictionary contains differential equations, with the key being the - # variable in the d/dt - self.rhs[Q] = I_dim * param.timescale / 3600 - # Initial conditions must be provided for the ODEs - self.initial_conditions[Q] = pybamm.Scalar(0) - - ###################### - # Interfacial reactions - ###################### - - c_s_surf_n = pybamm.surf(c_s_n) - phi_s_n = 0 - - j0_n = param.j0_n(1, c_s_surf_n, T) / param.C_r_n # set c_e = 1 - j_n = ( - 2 - * j0_n - * pybamm.sinh(param.ne_n / 2 * (phi_s_n - phi_e - param.U_n(c_s_surf_n, T))) - ) - c_s_surf_p = pybamm.surf(c_s_p) - j0_p = ( - param.gamma_p * param.j0_p(1, c_s_surf_p, T) / param.C_r_p - ) # setting c_e = 1 - - j_p = ( - 2 - * j0_p - * pybamm.sinh(param.ne_p / 2 * (phi_s_p - phi_e - param.U_p(c_s_surf_p, T))) - ) - - # integral equation for phi_e - self.algebraic[phi_e] = ( - pybamm.Integral(f_a_dist_n(R_variable_n, 1, sd_a_n) * j_n, R_variable_n) - - i_cell / param.l_n - ) - - # integral equation for phi_s_p - self.algebraic[phi_s_p] = ( - pybamm.Integral(f_a_dist_p(R_variable_p, 1, sd_a_p) * j_p, R_variable_p) - + i_cell / param.l_p - ) - - self.initial_conditions[phi_e] = pybamm.Scalar( - 1 - ) # pybamm.PrimaryBroadcast(1, "negative particle-size domain") - self.initial_conditions[phi_s_p] = pybamm.Scalar( - 1 - ) # pybamm.PrimaryBroadcast(1, "positive particle-size domain") - - ###################### - # Particles - ###################### - - # The div and grad operators will be converted to the appropriate matrix - # multiplication at the discretisation stage - N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n) - N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p) - self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n) / R_variable_n ** 2 - self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p) / R_variable_p ** 2 - - # Boundary conditions must be provided for equations with spatial derivatives - self.boundary_conditions[c_s_n] = { - "left": (pybamm.Scalar(0), "Neumann"), - "right": ( - -R_variable_n * param.C_n * j_n / param.a_n / param.D_n(c_s_surf_n, T), - "Neumann", - ), - } - self.boundary_conditions[c_s_p] = { - "left": (pybamm.Scalar(0), "Neumann"), - "right": ( - -R_variable_p - * param.C_p - * j_p - / param.a_p - / param.gamma_p - / param.D_p(c_s_surf_p, T), - "Neumann", - ), - } - # c_n_init and c_p_init are functions of x, but for the SPM we evaluate them at x=0 - # and x=1 since there is no x-dependence in the particles - self.initial_conditions[c_s_n] = param.c_n_init(0) - self.initial_conditions[c_s_p] = param.c_p_init(1) - - # Events specify points at which a solution should terminate - self.events += [ - pybamm.Event( - "Minimum negative particle surface concentration", - pybamm.min(c_s_surf_n) - 0.01, - ), - pybamm.Event( - "Maximum negative particle surface concentration", - (1 - 0.01) - pybamm.max(c_s_surf_n), - ), - pybamm.Event( - "Minimum positive particle surface concentration", - pybamm.min(c_s_surf_p) - 0.01, - ), - pybamm.Event( - "Maximum positive particle surface concentration", - (1 - 0.01) - pybamm.max(c_s_surf_p), - ), - ] - - # The `variables` dictionary contains all variables that might be useful for - # visualising the solution of the model - # Primary broadcasts are used to broadcast scalar quantities across a domain - # into a vector of the right shape, for multiplying with other vectors - - # Time and space output variables - self.set_standard_output_variables() - - # Dimensionless output variables (not already defined) - V = phi_s_p - c_e = 1 - - c_s_n_size_av = pybamm.Integral( - f_a_dist_n(R_variable_n, 1, sd_a_n) * c_s_n, R_variable_n - ) - c_s_p_size_av = pybamm.Integral( - f_a_dist_p(R_variable_p, 1, sd_a_p) * c_s_p, R_variable_p - ) - c_s_surf_n_size_av = pybamm.Integral( - f_a_dist_n(R_variable_n, 1, sd_a_n) * c_s_surf_n, R_variable_n - ) - c_s_surf_p_size_av = pybamm.Integral( - f_a_dist_p(R_variable_p, 1, sd_a_p) * c_s_surf_p, R_variable_p - ) - # Dimensional output variables - V_dim = param.potential_scale * V + (param.U_p_ref - param.U_n_ref) - - c_s_n_dim = c_s_n * param.c_n_max - c_s_p_dim = c_s_p * param.c_p_max - c_s_surf_n_dim = c_s_surf_n * param.c_n_max - c_s_surf_p_dim = c_s_surf_p * param.c_p_max - - c_s_n_size_av_dim = c_s_n_size_av * param.c_n_max - c_s_p_size_av_dim = c_s_p_size_av * param.c_p_max - c_s_surf_n_size_av_dim = c_s_surf_n_size_av * param.c_n_max - c_s_surf_p_size_av_dim = c_s_surf_p_size_av * param.c_p_max - - c_e_dim = c_e * param.c_e_typ - phi_s_n_dim = phi_s_n * param.potential_scale - phi_s_p_dim = phi_s_p * param.potential_scale + (param.U_p_ref - param.U_n_ref) - phi_e_dim = phi_e * param.potential_scale - param.U_n_ref - - whole_cell = ["negative electrode", "separator", "positive electrode"] - - self.variables.update( - { - # (Some of) New "Distribution" variables, those depending on R_variable_n, R_variable_p - "Negative particle concentration distribution": c_s_n, - "Negative particle concentration distribution [mol.m-3]": c_s_n_dim, - "Negative particle surface concentration distribution": c_s_surf_n, - "Negative particle surface concentration distribution [mol.m-3]": c_s_surf_n_dim, - "Positive particle concentration distribution": c_s_p, - "Positive particle concentration distribution [mol.m-3]": c_s_p_dim, - "Positive particle surface concentration distribution": c_s_surf_p, - "Positive particle surface concentration distribution [mol.m-3]": c_s_surf_p_dim, - "Negative area-weighted particle-size distribution": f_a_dist_n( - R_variable_n, 1, sd_a_n - ), - "Positive area-weighted particle-size distribution": f_a_dist_p( - R_variable_p, 1, sd_a_p - ), - # Standard output quantities (no PSD) - "Negative particle concentration": c_s_n_size_av, - "Negative particle concentration [mol.m-3]": c_s_n_size_av_dim, - "Negative particle surface concentration": c_s_surf_n_size_av, - "Negative particle surface concentration [mol.m-3]": c_s_surf_n_size_av_dim, - "Electrolyte concentration": pybamm.PrimaryBroadcast(c_e, whole_cell), - "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast( - c_e_dim, whole_cell - ), - "Positive particle concentration": c_s_p_size_av, - "Positive particle concentration [mol.m-3]": c_s_p_size_av_dim, - "Positive particle surface concentration": c_s_surf_p_size_av, - "Positive particle surface concentration [mol.m-3]": c_s_surf_p_size_av_dim, - "Negative electrode potential": pybamm.PrimaryBroadcast( - phi_s_n, "negative electrode" - ), - "Negative electrode potential [V]": pybamm.PrimaryBroadcast( - phi_s_n_dim, "negative electrode" - ), - "Electrolyte potential": pybamm.PrimaryBroadcast(phi_e, whole_cell), - "Electrolyte potential [V]": pybamm.PrimaryBroadcast( - phi_e_dim, whole_cell - ), - "Positive electrode potential": pybamm.PrimaryBroadcast( - phi_s_p, "positive electrode" - ), - "Positive electrode potential [V]": pybamm.PrimaryBroadcast( - phi_s_p_dim, "positive electrode" - ), - "Current": i_cell, - "Current [A]": I_dim, - "Terminal voltage": V, - "Terminal voltage [V]": V_dim, - } - ) - - self.events += [ - pybamm.Event("Minimum voltage", V - param.voltage_low_cut), - pybamm.Event("Maximum voltage", V - param.voltage_high_cut), - ] - - def set_standard_output_variables(self): - # This overwrites the method in parent class, base_lithium_ion_model.BaseModel, - # adding "particle-size variables" R_variable_n and R_variable_p - - # Time - self.variables.update( - { - "Time": pybamm.t, - "Time [s]": pybamm.t * self.timescale, - "Time [min]": pybamm.t * self.timescale / 60, - "Time [h]": pybamm.t * self.timescale / 3600, - } - ) - - # Spatial - var = pybamm.standard_spatial_vars - L_x = pybamm.geometric_parameters.L_x - self.variables.update( - { - "x": var.x, - "x [m]": var.x * L_x, - "x_n": var.x_n, - "x_n [m]": var.x_n * L_x, - "x_s": var.x_s, - "x_s [m]": var.x_s * L_x, - "x_p": var.x_p, - "x_p [m]": var.x_p * L_x, - } - ) - - # New Spatial Variables - R_variable_n = pybamm.SpatialVariable( - "negative particle-size variable", - domain=["negative particle-size domain"], - coord_sys="cartesian", - ) - R_variable_p = pybamm.SpatialVariable( - "positive particle-size variable", - domain=["positive particle-size domain"], - coord_sys="cartesian", - ) - R_n = pybamm.geometric_parameters.R_n - R_p = pybamm.geometric_parameters.R_p - - self.variables.update( - { - "Negative particle size": R_variable_n, - "Negative particle size [m]": R_variable_n * R_n, - "Positive particle size": R_variable_p, - "Positive particle size [m]": R_variable_p * R_p, - } - ) - - #################### - # Overwrite defaults - #################### - @property - def default_parameter_values(self): - # Default parameter values - # Lion parameters left as default parameter set for tests - default_params = super().default_parameter_values - - # New parameter values - # Area-weighted standard deviations (dimensionless) - sd_a_n = 0.5 - sd_a_p = 0.3 - # Max radius in the particle-size distribution (dimensionless) - R_n_max = max(2, 1 + sd_a_n * 5) - R_p_max = max(2, 1 + sd_a_p * 5) - # lognormal area-weighted particle-size distribution - - def lognormal_distribution(R, R_av, sd): - import numpy as np - - # inputs are particle radius R, the mean R_av, and standard deviation sd - # inputs can be dimensional or dimensionless - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / R - ) - - default_params.update( - {"negative area-weighted particle-size standard deviation": sd_a_n}, - check_already_exists=False, - ) - default_params.update( - {"positive area-weighted particle-size standard deviation": sd_a_p}, - check_already_exists=False, - ) - default_params.update( - {"negative maximum particle radius": R_n_max}, check_already_exists=False - ) - default_params.update( - {"positive maximum particle radius": R_p_max}, check_already_exists=False - ) - default_params.update( - { - "negative area-weighted particle-size distribution": lognormal_distribution - }, - check_already_exists=False, - ) - default_params.update( - { - "positive area-weighted particle-size distribution": lognormal_distribution - }, - check_already_exists=False, - ) - - return default_params - - @property - def default_geometry(self): - default_geom = super().default_geometry - - # New Spatial Variables - R_variable_n = pybamm.SpatialVariable( - "negative particle-size variable", - domain=["negative particle-size domain"], - coord_sys="cartesian", - ) - R_variable_p = pybamm.SpatialVariable( - "positive particle-size variable", - domain=["positive particle-size domain"], - coord_sys="cartesian", - ) - - # append new domains - default_geom.update( - { - "negative particle-size domain": { - R_variable_n: { - "min": pybamm.Scalar(0), - "max": pybamm.Parameter("negative maximum particle radius"), - } - }, - "positive particle-size domain": { - R_variable_p: { - "min": pybamm.Scalar(0), - "max": pybamm.Parameter("negative maximum particle radius"), - } - }, - } - ) - return default_geom - - @property - def default_var_pts(self): - defaults = super().default_var_pts - - # New Spatial Variables - R_variable_n = pybamm.SpatialVariable( - "negative particle-size variable", - domain=["negative particle-size domain"], - coord_sys="cartesian", - ) - R_variable_p = pybamm.SpatialVariable( - "positive particle-size variable", - domain=["positive particle-size domain"], - coord_sys="cartesian", - ) - # add to dictionary - defaults.update({R_variable_n: 50, R_variable_p: 50}) - return defaults - - @property - def default_submesh_types(self): - default_submeshes = super().default_submesh_types - - default_submeshes.update( - { - "negative particle-size domain": pybamm.MeshGenerator( - pybamm.Uniform1DSubMesh - ), - "positive particle-size domain": pybamm.MeshGenerator( - pybamm.Uniform1DSubMesh - ), - } - ) - return default_submeshes - - @property - def default_spatial_methods(self): - default_spatials = super().default_spatial_methods - - default_spatials.update( - { - "negative particle-size domain": pybamm.FiniteVolume(), - "positive particle-size domain": pybamm.FiniteVolume(), - } - ) - return default_spatials - - def new_copy(self, build=False): - return pybamm.BaseModel.new_copy(self) \ No newline at end of file diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index 46af4a45b2..506288a472 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -122,19 +122,21 @@ def set_particle_submodel(self): if self.options["particle size"] == "distribution": if self.options["particle"] == "Fickian diffusion": - self.submodels["negative particle"] = pybamm.particle.FickianManyPSDs( + submod_n = pybamm.particle.FickianManySizeDistributions( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.FickianManyPSDs( + submod_p = pybamm.particle.FickianManySizeDistributions( self.param, "Positive" ) elif self.options["particle"] == "uniform profile": - self.submodels["negative particle"] = pybamm.particle.FastManyPSDs( + submod_n = pybamm.particle.FastManySizeDistributions( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.FastManyPSDs( + submod_p = pybamm.particle.FastManySizeDistributions( self.param, "Positive" ) + self.submodels["negative particle"] = submod_n + self.submodels["positive particle"] = submod_p else: if self.options["particle"] == "Fickian diffusion": self.submodels["negative particle"] = pybamm.particle.FickianManyParticles( diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 9f86df342f..88599111f9 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -139,19 +139,21 @@ def set_interfacial_submodel(self): def set_particle_submodel(self): if self.options["particle"] == "Fickian diffusion": - self.submodels["negative particle"] = pybamm.particle.FickianSinglePSD( + submod_n = pybamm.particle.FickianSingleSizeDistribution( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.FickianSinglePSD( + submod_p = pybamm.particle.FickianSingleSizeDistribution( self.param, "Positive" ) - elif self.options["particle"] == "fast diffusion": - self.submodels["negative particle"] = pybamm.particle.FastSinglePSD( + elif self.options["particle"] == "uniform profile": + submod_n = pybamm.particle.FastSingleSizeDistribution( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.FastSinglePSD( + submod_p = pybamm.particle.FastSingleSizeDistribution( self.param, "Positive" ) + self.submodels["negative particle"] = submod_n + self.submodels["positive particle"] = submod_p def set_negative_electrode_submodel(self): diff --git a/pybamm/models/submodels/particle/__init__.py b/pybamm/models/submodels/particle/__init__.py index 34cd544f7b..b60cdfde22 100644 --- a/pybamm/models/submodels/particle/__init__.py +++ b/pybamm/models/submodels/particle/__init__.py @@ -1,9 +1,6 @@ from .base_particle import BaseParticle from .fickian_many_particles import FickianManyParticles -from .fickian_many_particle_size_distributions import FickianManyPSDs from .fickian_single_particle import FickianSingleParticle -from .fickian_single_particle_size_distribution import FickianSinglePSD -from .fast_many_particle_size_distributions import FastManyPSDs -from .fast_single_particle_size_distribution import FastSinglePSD from .polynomial_single_particle import PolynomialSingleParticle from .polynomial_many_particles import PolynomialManyParticles +from .size_distribution import * diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index 772a4229f4..ee27c60b21 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -144,117 +144,3 @@ def _get_standard_flux_variables(self, N_s, N_s_xav): return variables - def _get_standard_concentration_distribution_variables(self, c_s): - """ - Forms standard concentration variables that depend on particle size R given - one concentration distribution variable c_s. - """ - if self.domain == "Negative": - c_scale = self.param.c_n_max - elif self.domain == "Positive": - c_scale = self.param.c_p_max - - # Note: Currently not possible to broadcast from (r, R) to (r, R, x) since - # domain x for broadcast is in "tertiary" position. - - # Broadcast and x-average when necessary - if c_s.domain == [ - self.domain.lower() + " particle size" - ] and c_s.auxiliary_domains["secondary"] != [ - self.domain.lower() + " electrode" - ]: - c_s_xav_distribution = pybamm.PrimaryBroadcast( - c_s, [self.domain.lower() + " particle"] - ) - # Placeholder broadcast to x, filled with zeros only - c_s_distribution = pybamm.FullBroadcast( - 0, - [self.domain.lower() + " particle"], - { - "secondary": self.domain.lower() + " particle size", - "tertiary": self.domain.lower() + " electrode", - }, - ) - - # Surface concentration distribution variables - c_s_surf_xav_distribution = c_s - c_s_surf_distribution = pybamm.SecondaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] - ) - elif c_s.domain == [self.domain.lower() + " particle"] and ( - c_s.auxiliary_domains["tertiary"] != [self.domain.lower() + " electrode"] - ): - c_s_xav_distribution = c_s - # Placeholder broadcast to x, filled with zeros only - c_s_distribution = pybamm.FullBroadcast( - 0, - [self.domain.lower() + " particle"], - { - "secondary": self.domain.lower() + " particle size", - "tertiary": self.domain.lower() + " electrode", - }, - ) - - # Surface concentration distribution variables - c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) - c_s_surf_distribution = pybamm.SecondaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] - ) - elif c_s.domain == [ - self.domain.lower() + " particle size" - ] and c_s.auxiliary_domains["secondary"] == [ - self.domain.lower() + " electrode" - ]: - c_s_surf_distribution = c_s - c_s_surf_xav_distribution = pybamm.x_average(c_s) - - c_s_xav_distribution = pybamm.PrimaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " particle"] - ) - # Placeholder broadcast to x, filled with zeros only - c_s_distribution = pybamm.FullBroadcast( - 0, - [self.domain.lower() + " particle"], - { - "secondary": self.domain.lower() + " particle size", - "tertiary": self.domain.lower() + " electrode", - }, - ) - else: - c_s_distribution = c_s - # TODO: x-average the *tertiary* domain. Leave unaltered for now. - c_s_xav_distribution = c_s - - # Surface concentration distribution variables - c_s_surf_distribution = pybamm.surf(c_s) - c_s_surf_xav_distribution = pybamm.x_average(c_s_surf_distribution) - - variables = { - self.domain - + " particle concentration distribution": c_s_distribution, - self.domain - + " particle concentration distribution " - + "[mol.m-3]": c_scale * c_s_distribution, - "X-averaged " - + self.domain.lower() - + " particle concentration distribution": c_s_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle concentration distribution " - + "[mol.m-3]": c_scale * c_s_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle surface concentration" - + " distribution": c_s_surf_xav_distribution, - "X-averaged " - + self.domain.lower() - + " particle surface concentration distribution " - + "[mol.m-3]": c_scale * c_s_surf_xav_distribution, - self.domain - + " particle surface concentration" - + " distribution": c_s_surf_distribution, - self.domain - + " particle surface concentration" - + " distribution [mol.m-3]": c_scale * c_s_surf_distribution, - } - return variables diff --git a/pybamm/models/submodels/particle/size_distribution/__init__.py b/pybamm/models/submodels/particle/size_distribution/__init__.py new file mode 100644 index 0000000000..6ef02d5297 --- /dev/null +++ b/pybamm/models/submodels/particle/size_distribution/__init__.py @@ -0,0 +1,5 @@ +from .base_distribution import BaseSizeDistribution +from .fickian_many_distributions import FickianManySizeDistributions +from .fickian_single_distribution import FickianSingleSizeDistribution +from .fast_many_distributions import FastManySizeDistributions +from .fast_single_distribution import FastSingleSizeDistribution diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py new file mode 100644 index 0000000000..da70c96f93 --- /dev/null +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -0,0 +1,139 @@ +# +# Base class for particles +# +import pybamm + +from ..base_particle import BaseParticle + + +class BaseSizeDistribution(BaseParticle): + """ + Base class for molar conservation in a distribution of particle sizes. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + domain : str + The domain of the model either 'Negative' or 'Positive' + + **Extends:** :class:`pybamm.BaseParticle` + """ + + def __init__(self, param, domain): + super().__init__(param, domain) + + def _get_standard_concentration_distribution_variables(self, c_s): + """ + Forms standard concentration variables that depend on particle size R given + one concentration distribution variable c_s. + """ + if self.domain == "Negative": + c_scale = self.param.c_n_max + elif self.domain == "Positive": + c_scale = self.param.c_p_max + + # Note: Currently not possible to broadcast from (r, R) to (r, R, x) since + # domain x for broadcast is in "tertiary" position. + + # Broadcast and x-average when necessary + if c_s.domain == [ + self.domain.lower() + " particle size" + ] and c_s.auxiliary_domains["secondary"] != [ + self.domain.lower() + " electrode" + ]: + c_s_xav_distribution = pybamm.PrimaryBroadcast( + c_s, [self.domain.lower() + " particle"] + ) + # Placeholder broadcast to x, filled with zeros only + c_s_distribution = pybamm.FullBroadcast( + 0, + [self.domain.lower() + " particle"], + { + "secondary": self.domain.lower() + " particle size", + "tertiary": self.domain.lower() + " electrode", + }, + ) + + # Surface concentration distribution variables + c_s_surf_xav_distribution = c_s + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + elif c_s.domain == [self.domain.lower() + " particle"] and ( + c_s.auxiliary_domains["tertiary"] != [self.domain.lower() + " electrode"] + ): + c_s_xav_distribution = c_s + # Placeholder broadcast to x, filled with zeros only + c_s_distribution = pybamm.FullBroadcast( + 0, + [self.domain.lower() + " particle"], + { + "secondary": self.domain.lower() + " particle size", + "tertiary": self.domain.lower() + " electrode", + }, + ) + + # Surface concentration distribution variables + c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + elif c_s.domain == [ + self.domain.lower() + " particle size" + ] and c_s.auxiliary_domains["secondary"] == [ + self.domain.lower() + " electrode" + ]: + c_s_surf_distribution = c_s + c_s_surf_xav_distribution = pybamm.x_average(c_s) + + c_s_xav_distribution = pybamm.PrimaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " particle"] + ) + # Placeholder broadcast to x, filled with zeros only + c_s_distribution = pybamm.FullBroadcast( + 0, + [self.domain.lower() + " particle"], + { + "secondary": self.domain.lower() + " particle size", + "tertiary": self.domain.lower() + " electrode", + }, + ) + else: + c_s_distribution = c_s + # TODO: x-average the *tertiary* domain. Leave unaltered for now. + c_s_xav_distribution = c_s + + # Surface concentration distribution variables + c_s_surf_distribution = pybamm.surf(c_s) + c_s_surf_xav_distribution = pybamm.x_average(c_s_surf_distribution) + + variables = { + self.domain + + " particle concentration distribution": c_s_distribution, + self.domain + + " particle concentration distribution " + + "[mol.m-3]": c_scale * c_s_distribution, + "X-averaged " + + self.domain.lower() + + " particle concentration distribution": c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle concentration distribution " + + "[mol.m-3]": c_scale * c_s_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration" + + " distribution": c_s_surf_xav_distribution, + "X-averaged " + + self.domain.lower() + + " particle surface concentration distribution " + + "[mol.m-3]": c_scale * c_s_surf_xav_distribution, + self.domain + + " particle surface concentration" + + " distribution": c_s_surf_distribution, + self.domain + + " particle surface concentration" + + " distribution [mol.m-3]": c_scale * c_s_surf_distribution, + } + return variables diff --git a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py similarity index 97% rename from pybamm/models/submodels/particle/fast_many_particle_size_distributions.py rename to pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index 74d0867250..deb3cdeee7 100644 --- a/pybamm/models/submodels/particle/fast_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -5,10 +5,10 @@ # import pybamm -from .base_particle import BaseParticle +from .base_distribution import BaseSizeDistribution -class FastManyPSDs(BaseParticle): +class FastManySizeDistributions(BaseSizeDistribution): """Class for molar conservation in many particle-size distributions (PSD), one distribution at every x location of the electrode, with fast diffusion (uniform concentration in r) within the particles @@ -21,7 +21,7 @@ class FastManyPSDs(BaseParticle): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.BaseParticle` + **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py similarity index 96% rename from pybamm/models/submodels/particle/fast_single_particle_size_distribution.py rename to pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index 0a461449ff..f63c8e484c 100644 --- a/pybamm/models/submodels/particle/fast_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -4,12 +4,12 @@ # import pybamm -from .base_particle import BaseParticle +from .base_distribution import BaseSizeDistribution -class FastSinglePSD(BaseParticle): +class FastSingleSizeDistribution(BaseSizeDistribution): """Class for molar conservation in a single (i.e., x-averaged) particle-size - distribution (PSD) with fast diffusion within each particle + distribution) with fast diffusion within each particle (uniform concentration in r). Parameters @@ -20,7 +20,7 @@ class FastSinglePSD(BaseParticle): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.BaseParticle` + **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py similarity index 97% rename from pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py rename to pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py index 2523197276..320c8621cc 100644 --- a/pybamm/models/submodels/particle/fickian_many_particle_size_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py @@ -4,12 +4,12 @@ # import pybamm -from .base_particle import BaseParticle +from .base_distribution import BaseSizeDistribution -class FickianManyPSDs(BaseParticle): +class FickianManySizeDistributions(BaseSizeDistribution): """Class for molar conservation in many particle-size - distributions (PSD), one distribution at every x location of the electrode, + distributions, one distribution at every x location of the electrode, with Fickian diffusion within each particle. Parameters @@ -20,7 +20,7 @@ class FickianManyPSDs(BaseParticle): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.BaseParticle` + **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py similarity index 97% rename from pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py rename to pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index d50e4729f2..5427824871 100644 --- a/pybamm/models/submodels/particle/fickian_single_particle_size_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -4,12 +4,12 @@ # import pybamm -from .base_particle import BaseParticle +from .base_distribution import BaseSizeDistribution -class FickianSinglePSD(BaseParticle): +class FickianSingleSizeDistribution(BaseSizeDistribution): """Class for molar conservation in a single (i.e., x-averaged) particle-size - distribution (PSD) with Fickian diffusion within each particle. + distribution with Fickian diffusion within each particle. Parameters ---------- @@ -19,7 +19,7 @@ class FickianSinglePSD(BaseParticle): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.BaseParticle` + **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` """ def __init__(self, param, domain): From 0a379ab80ca3ee6c990324375ddc731074edb147 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 4 Jun 2021 13:18:49 +0100 Subject: [PATCH 27/67] fix fast diffusion MP submodels and output vars --- pybamm/geometry/standard_spatial_vars.py | 24 ++++--- .../size_distribution/base_distribution.py | 63 +++++++++++-------- .../fast_many_distributions.py | 28 ++++----- .../fast_single_distribution.py | 43 +++++++++---- .../fickian_single_distribution.py | 18 +++--- 5 files changed, 102 insertions(+), 74 deletions(-) diff --git a/pybamm/geometry/standard_spatial_vars.py b/pybamm/geometry/standard_spatial_vars.py index b2752246a7..4bec1f8bc6 100644 --- a/pybamm/geometry/standard_spatial_vars.py +++ b/pybamm/geometry/standard_spatial_vars.py @@ -53,19 +53,19 @@ R_n = pybamm.SpatialVariable( "R_n", domain=["negative particle size"], - # auxiliary_domains={ - # "secondary": "negative electrode", - # "tertiary": "current collector", - # }, + auxiliary_domains={ + "secondary": "negative electrode", + "tertiary": "current collector", + }, coord_sys="cartesian", ) R_p = pybamm.SpatialVariable( "R_p", domain=["positive particle size"], - # auxiliary_domains={ - # "secondary": "positive electrode", - # "tertiary": "current collector", - # }, + auxiliary_domains={ + "secondary": "positive electrode", + "tertiary": "current collector", + }, coord_sys="cartesian", ) @@ -124,10 +124,18 @@ R_n_edge = pybamm.SpatialVariableEdge( "R_n", domain=["negative particle size"], + auxiliary_domains={ + "secondary": "negative electrode", + "tertiary": "current collector", + }, coord_sys="cartesian", ) R_p_edge = pybamm.SpatialVariableEdge( "R_p", domain=["positive particle size"], + auxiliary_domains={ + "secondary": "positive electrode", + "tertiary": "current collector", + }, coord_sys="cartesian", ) diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index da70c96f93..5f7e8e750e 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -26,7 +26,7 @@ def __init__(self, param, domain): def _get_standard_concentration_distribution_variables(self, c_s): """ Forms standard concentration variables that depend on particle size R given - one concentration distribution variable c_s. + the fundamental concentration distribution variable c_s from the submodel. """ if self.domain == "Negative": c_scale = self.param.c_n_max @@ -42,29 +42,38 @@ def _get_standard_concentration_distribution_variables(self, c_s): ] and c_s.auxiliary_domains["secondary"] != [ self.domain.lower() + " electrode" ]: + # X-avg concentration distribution c_s_xav_distribution = pybamm.PrimaryBroadcast( c_s, [self.domain.lower() + " particle"] ) - # Placeholder broadcast to x, filled with zeros only - c_s_distribution = pybamm.FullBroadcast( - 0, - [self.domain.lower() + " particle"], - { - "secondary": self.domain.lower() + " particle size", - "tertiary": self.domain.lower() + " electrode", - }, - ) # Surface concentration distribution variables c_s_surf_xav_distribution = c_s c_s_surf_distribution = pybamm.SecondaryBroadcast( c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] ) + + # Concentration distribution in all domains. + # NOTE: currently variables can only have 3 domains, so current collector + # is excluded, i.e. pushed off domain list + c_s_distribution = pybamm.PrimaryBroadcast( + c_s_surf_distribution, [self.domain.lower() + " particle"] + ) elif c_s.domain == [self.domain.lower() + " particle"] and ( c_s.auxiliary_domains["tertiary"] != [self.domain.lower() + " electrode"] ): + # X-avg concentration distribution c_s_xav_distribution = c_s - # Placeholder broadcast to x, filled with zeros only + + # Surface concentration distribution variables + c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) + c_s_surf_distribution = pybamm.SecondaryBroadcast( + c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] + ) + + # Concentration distribution in all domains. + # NOTE: requires broadcast to "tertiary" domain, which is + # not implemented. Fill with zeros instead as placeholder c_s_distribution = pybamm.FullBroadcast( 0, [self.domain.lower() + " particle"], @@ -73,36 +82,36 @@ def _get_standard_concentration_distribution_variables(self, c_s): "tertiary": self.domain.lower() + " electrode", }, ) - - # Surface concentration distribution variables - c_s_surf_xav_distribution = pybamm.surf(c_s_xav_distribution) - c_s_surf_distribution = pybamm.SecondaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " electrode"] - ) elif c_s.domain == [ self.domain.lower() + " particle size" ] and c_s.auxiliary_domains["secondary"] == [ self.domain.lower() + " electrode" ]: + # Surface concentration distribution variables c_s_surf_distribution = c_s c_s_surf_xav_distribution = pybamm.x_average(c_s) + # X-avg concentration distribution c_s_xav_distribution = pybamm.PrimaryBroadcast( c_s_surf_xav_distribution, [self.domain.lower() + " particle"] ) - # Placeholder broadcast to x, filled with zeros only - c_s_distribution = pybamm.FullBroadcast( - 0, - [self.domain.lower() + " particle"], - { - "secondary": self.domain.lower() + " particle size", - "tertiary": self.domain.lower() + " electrode", - }, + + # Concentration distribution in all domains. + # NOTE: currently variables can only have 3 domains, so current collector + # is excluded, i.e. pushed off domain list + c_s_distribution = pybamm.PrimaryBroadcast( + c_s_surf_distribution, [self.domain.lower() + " particle"] ) else: c_s_distribution = c_s - # TODO: x-average the *tertiary* domain. Leave unaltered for now. - c_s_xav_distribution = c_s + + # x-average the *tertiary* domain. Do manually using Integral + #x = pybamm.standard_spatial_vars.x_p + #l = pybamm.geometric_parameters.l_p + x = pybamm.SpatialVariable("x", domain=[self.domain.lower() + " electrode"]) + v = pybamm.ones_like(c_s) + l = pybamm.Integral(v, x) + c_s_xav_distribution = pybamm.Integral(c_s, x) / l # Surface concentration distribution variables c_s_surf_distribution = pybamm.surf(c_s) diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index deb3cdeee7..2997189285 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -10,7 +10,7 @@ class FastManySizeDistributions(BaseSizeDistribution): """Class for molar conservation in many particle-size - distributions (PSD), one distribution at every x location of the electrode, + distributions, one distribution at every x location of the electrode, with fast diffusion (uniform concentration in r) within the particles Parameters @@ -30,8 +30,7 @@ def __init__(self, param, domain): def get_fundamental_variables(self): # The concentration is uniform throughout each particle, so we - # can just use the surface value. This avoids dealing with - # x, R *and* r averaged quantities, which may be confusing. + # can just use the surface value. if self.domain == "Negative": # distribution variables @@ -44,13 +43,11 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_n # used for averaging - R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) - R_variable = pybamm.SecondaryBroadcast(R_variable, ["negative electrode"]) + R = pybamm.standard_spatial_vars.R_n R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_variable) + f_a_dist = self.param.f_a_dist_n(R) elif self.domain == "Positive": # distribution variables @@ -63,13 +60,11 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - R = pybamm.standard_spatial_vars.R_p # used for averaging - R_variable = pybamm.SecondaryBroadcast(R, ["current collector"]) - R_variable = pybamm.SecondaryBroadcast(R_variable, ["positive electrode"]) + R = pybamm.standard_spatial_vars.R_p R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_variable) + f_a_dist = self.param.f_a_dist_p(R) # Ensure the distribution is normalised, irrespective of discretisation # or user input @@ -105,8 +100,8 @@ def get_fundamental_variables(self): ) variables.update( { - self.domain + " particle size": R_variable, - self.domain + " particle size [m]": R_variable * R_dim, + self.domain + " particle size": R, + self.domain + " particle size [m]": R * R_dim, self.domain + " area-weighted particle-size" + " distribution": pybamm.x_average(f_a_dist), @@ -126,23 +121,22 @@ def set_rhs(self, variables): self.domain + " electrode interfacial current density distribution" ] - R_variable = variables[self.domain + " particle size"] + R = variables[self.domain + " particle size"] if self.domain == "Negative": self.rhs = { c_s_surf_distribution: -3 * j_distribution / self.param.a_R_n - / R_variable + / R } - elif self.domain == "Positive": self.rhs = { c_s_surf_distribution: -3 * j_distribution / self.param.a_R_p / self.param.gamma_p - / R_variable + / R } def set_initial_conditions(self, variables): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index f63c8e484c..ae58edc68b 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -29,8 +29,7 @@ def __init__(self, param, domain): def get_fundamental_variables(self): # The concentration is uniform throughout each particle, so we - # can just use the surface value. This avoids dealing with - # x, R *and* r averaged quantities, which may be confusing. + # can just use the surface value. if self.domain == "Negative": # distribution variables @@ -40,11 +39,19 @@ def get_fundamental_variables(self): auxiliary_domains={"secondary": "current collector"}, bounds=(0, 1), ) - R_variable = pybamm.standard_spatial_vars.R_n + # Since concentration does not depend on "x", need a particle-size + # spatial variable R with only "current collector" as secondary + # domain + R_spatial_variable = pybamm.SpatialVariable( + "R_n", + domain=["negative particle size"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", + ) R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_variable) + f_a_dist = self.param.f_a_dist_n(R_spatial_variable) elif self.domain == "Positive": # distribution variables @@ -54,15 +61,23 @@ def get_fundamental_variables(self): auxiliary_domains={"secondary": "current collector"}, bounds=(0, 1), ) - R_variable = pybamm.standard_spatial_vars.R_p + # Since concentration does not depend on "x", need a particle-size + # spatial variable R with only "current collector" as secondary + # domain + R_spatial_variable = pybamm.SpatialVariable( + "R_p", + domain=["positive particle size"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", + ) R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_variable) + f_a_dist = self.param.f_a_dist_p(R_spatial_variable) # Ensure the distribution is normalised, irrespective of discretisation # or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_spatial_variable) # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( @@ -78,7 +93,9 @@ def get_fundamental_variables(self): ) # Standard R-averaged variables - c_s_surf_xav = pybamm.Integral(f_a_dist * c_s_surf_xav_distribution, R_variable) + c_s_surf_xav = pybamm.Integral( + f_a_dist * c_s_surf_xav_distribution, R_spatial_variable + ) c_s_xav = pybamm.PrimaryBroadcast( c_s_surf_xav, [self.domain.lower() + " particle"] ) @@ -94,8 +111,8 @@ def get_fundamental_variables(self): ) variables.update( { - self.domain + " particle size": R_variable, - self.domain + " particle size [m]": R_variable * R_dim, + self.domain + " particle size": R_spatial_variable, + self.domain + " particle size [m]": R_spatial_variable * R_dim, self.domain + " area-weighted particle-size" + " distribution": f_a_dist, @@ -117,14 +134,14 @@ def set_rhs(self, variables): + self.domain.lower() + " electrode interfacial current density distribution" ] - R_variable = variables[self.domain + " particle size"] + R = variables[self.domain + " particle size"] if self.domain == "Negative": self.rhs = { c_s_surf_xav_distribution: -3 * j_xav_distribution / self.param.a_R_n - / R_variable + / R } elif self.domain == "Positive": @@ -133,7 +150,7 @@ def set_rhs(self, variables): * j_xav_distribution / self.param.a_R_p / self.param.gamma_p - / R_variable + / R } def set_initial_conditions(self, variables): diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index 5427824871..73a1423df5 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -41,7 +41,7 @@ def get_fundamental_variables(self): # Since concentration does not depend on "x", need a particle-size # spatial variable R with only "current collector" as secondary # domain - R_variable = pybamm.SpatialVariable( + R_spatial_variable = pybamm.SpatialVariable( "R_n", domain=["negative particle size"], auxiliary_domains={"secondary": "current collector"}, @@ -50,7 +50,7 @@ def get_fundamental_variables(self): R_dim = self.param.R_n_typ # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_variable) + f_a_dist = self.param.f_a_dist_n(R_spatial_variable) elif self.domain == "Positive": # distribution variables @@ -66,7 +66,7 @@ def get_fundamental_variables(self): # Since concentration does not depend on "x", need a particle-size # spatial variable R with only "current collector" as secondary # domain - R_variable = pybamm.SpatialVariable( + R_spatial_variable = pybamm.SpatialVariable( "R_p", domain=["positive particle size"], auxiliary_domains={"secondary": "current collector"}, @@ -75,14 +75,14 @@ def get_fundamental_variables(self): R_dim = self.param.R_p_typ # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_variable) + f_a_dist = self.param.f_a_dist_p(R_spatial_variable) # Ensure the distribution is normalised, irrespective of discretisation # or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_spatial_variable) # Standard R-averaged variables (avg secondary domain) - c_s_xav = pybamm.Integral(f_a_dist * c_s_xav_distribution, R_variable) + c_s_xav = pybamm.Integral(f_a_dist * c_s_xav_distribution, R_spatial_variable) c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) variables = self._get_standard_concentration_variables(c_s, c_s_xav) @@ -94,8 +94,8 @@ def get_fundamental_variables(self): ) variables.update( { - self.domain + " particle size": R_variable, - self.domain + " particle size [m]": R_variable * R_dim, + self.domain + " particle size": R_spatial_variable, + self.domain + " particle size [m]": R_spatial_variable * R_dim, self.domain + " area-weighted particle-size" + " distribution": f_a_dist, self.domain + " area-weighted particle-size" @@ -139,7 +139,7 @@ def get_coupled_variables(self, variables): variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) # Standard distribution flux variables (R-dependent) - # (Cannot currently broadcast to "x" as it is a tertiary domain) + # (Cannot currently broadcast to "x" as cannot have 4 domains) variables.update( { "X-averaged " From ba8988b6ef2022105bd86bf44bd74fa6c021fa00 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 4 Jun 2021 14:12:09 +0100 Subject: [PATCH 28/67] fix tests --- .../interface/inverse_kinetics/inverse_butler_volmer.py | 2 +- .../test_submodels/test_interface/test_butler_volmer.py | 6 ++++++ .../test_full_battery_models/test_base_battery_model.py | 1 + .../test_interface/test_kinetics/test_butler_volmer.py | 2 ++ .../test_interface/test_kinetics/test_tafel.py | 2 ++ .../test_submodels/test_interface/test_lithium_ion.py | 2 ++ 6 files changed, 14 insertions(+), 1 deletion(-) 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 be25244b99..32065f06ca 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -32,7 +32,7 @@ def __init__(self, param, domain, reaction, options=None): if options is None: options = { "SEI film resistance": "none", - "particle size": "distribution" + "particle size": "single" } self.options = options diff --git a/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py b/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py index 121543b64d..16bbaa62d2 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py @@ -82,6 +82,7 @@ def test_creation(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) j_n = model_n.get_coupled_variables(self.variables)[ @@ -94,6 +95,7 @@ def test_creation(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) j_p = model_p.get_coupled_variables(self.variables)[ @@ -117,6 +119,7 @@ def test_set_parameters(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) j_n = model_n.get_coupled_variables(self.variables)[ @@ -129,6 +132,7 @@ def test_set_parameters(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) j_p = model_p.get_coupled_variables(self.variables)[ @@ -155,6 +159,7 @@ def test_discretisation(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) j_n = model_n.get_coupled_variables(self.variables)[ @@ -167,6 +172,7 @@ def test_discretisation(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) j_p = model_p.get_coupled_variables(self.variables)[ 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 e890d23c9e..d36bb0518b 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 @@ -23,6 +23,7 @@ 'current collector': 'uniform' (possible: ['uniform', 'potential pair', 'potential pair quite conductive']) 'particle': 'Fickian diffusion' (possible: ['Fickian diffusion', 'fast diffusion', 'uniform profile', 'quadratic profile', 'quartic profile']) 'particle shape': 'spherical' (possible: ['spherical', 'user', 'no particles']) +'particle size': 'single' (possible: ['single', 'distribution']) 'electrolyte conductivity': 'default' (possible: ['default', 'full', 'leading order', 'composite', 'integrated']) 'thermal': 'x-full' (possible: ['isothermal', 'lumped', 'x-lumped', 'x-full']) 'cell geometry': 'pouch' (possible: ['arbitrary', 'pouch']) diff --git a/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_butler_volmer.py b/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_butler_volmer.py index d1f3f53304..e59a5f6240 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_butler_volmer.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_butler_volmer.py @@ -35,6 +35,7 @@ def test_public_functions(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) std_tests = tests.StandardSubModelTests(submodel, variables) @@ -75,6 +76,7 @@ def test_public_functions(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) std_tests = tests.StandardSubModelTests(submodel, variables) diff --git a/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_tafel.py b/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_tafel.py index 9dadf64e88..4e0bbc0834 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_tafel.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_kinetics/test_tafel.py @@ -35,6 +35,7 @@ def test_public_function(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) std_tests = tests.StandardSubModelTests(submodel, variables) @@ -76,6 +77,7 @@ def test_public_function(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) std_tests = tests.StandardSubModelTests(submodel, variables) diff --git a/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py b/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py index baa1f9e0a5..80504d8e48 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py @@ -35,6 +35,7 @@ def test_public_functions(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) std_tests = tests.StandardSubModelTests(submodel, variables) @@ -75,6 +76,7 @@ def test_public_functions(self): { "SEI film resistance": "none", "total interfacial current density as a state": "false", + "particle size": "single" }, ) std_tests = tests.StandardSubModelTests(submodel, variables) From 003bff666bdddf87d8e457d83d9434e0d5755eab Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 4 Jun 2021 16:01:17 +0100 Subject: [PATCH 29/67] added citations to Kirk et al 2020 --- pybamm/CITATIONS.txt | 11 +++++++++++ pybamm/models/full_battery_models/lithium_ion/mpm.py | 2 +- .../size_distribution/fast_single_distribution.py | 2 +- .../size_distribution/fickian_single_distribution.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pybamm/CITATIONS.txt b/pybamm/CITATIONS.txt index 61ccc186d4..332b656602 100644 --- a/pybamm/CITATIONS.txt +++ b/pybamm/CITATIONS.txt @@ -394,3 +394,14 @@ doi={10.1149/2.0661810jes} year={2020}, publisher={Elsevier} } + +@misc{Kirk2020, + author = {Kirk, Toby L. and Evans, Jack and Please, Colin P. and Chapman, S. Jonathan}, + title = {Modelling electrode heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size distributions}, + year = {2020}, + howpublished = {arXiv:2006.12208}, + eprint = {2006.12208}, + url = {https://arxiv.org/abs/2006.12208}, + archiveprefix = {arXiv}, + primaryclass = {physics.app-ph}, +} \ No newline at end of file diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 88599111f9..14b6c4fed2 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -59,7 +59,7 @@ def __init__( if build: self.build_model() - # pybamm.citations.register("marquis2019asymptotic") + pybamm.citations.register("Kirk2020") def set_porosity_submodel(self): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index ae58edc68b..3d44386c1b 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -25,7 +25,7 @@ class FastSingleSizeDistribution(BaseSizeDistribution): def __init__(self, param, domain): super().__init__(param, domain) - # pybamm.citations.register("kirk2020") + pybamm.citations.register("Kirk2020") def get_fundamental_variables(self): # The concentration is uniform throughout each particle, so we diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index 73a1423df5..e416f7d94f 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -24,7 +24,7 @@ class FickianSingleSizeDistribution(BaseSizeDistribution): def __init__(self, param, domain): super().__init__(param, domain) - # pybamm.citations.register("kirk2020") + pybamm.citations.register("Kirk2020") def get_fundamental_variables(self): if self.domain == "Negative": From 9050cf2a8d6aed7f81368d67d95c767b033f6d1c Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 8 Jun 2021 13:44:16 +0100 Subject: [PATCH 30/67] add unit tests for MPM and submodels --- pybamm/expression_tree/unary_operators.py | 71 ++-- .../full_battery_models/lithium_ion/mpm.py | 90 ++++- .../ohm/leading_size_distribution_ohm.py | 20 +- .../submodels/interface/base_interface.py | 10 +- .../size_distribution/base_distribution.py | 3 - pybamm/solvers/processed_variable.py | 10 +- .../test_lithium_ion/test_mpm.py | 323 ++++++++++++++++++ .../test_unary_operators.py | 52 +++ .../test_geometry/test_battery_geometry.py | 14 +- .../test_lithium_ion/test_mpm.py | 273 +++++++++++++++ .../test_leading_size_distribution.py | 52 +++ .../test_interface/test_size_distribution.py | 110 ++++++ .../test_size_distribution/__init__.py | 0 .../test_base_distribution.py | 32 ++ .../test_fast_many_distributions.py | 57 ++++ .../test_fast_single_distribution.py | 50 +++ .../test_fickian_many_distributions.py | 52 +++ .../test_fickian_single_distribution.py | 44 +++ 18 files changed, 1207 insertions(+), 56 deletions(-) create mode 100644 tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py create mode 100644 tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py create mode 100644 tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py create mode 100644 tests/unit/test_models/test_submodels/test_interface/test_size_distribution.py create mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/__init__.py create mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_base_distribution.py create mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py create mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_single_distribution.py create mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py create mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_single_distribution.py diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 2830c06d8b..bc5b332997 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1344,18 +1344,16 @@ def r_average(symbol): return Integral(symbol, r) / Integral(v, r) -def R_average(symbol, domain, param): +def R_average(symbol, param): """convenience function for averaging over particle size R. Parameters ---------- symbol : :class:`pybamm.Symbol` The function to be averaged - domain : str - The electrode for averaging, either "negative" or "positive" param : :class:`pybamm.LithiumIonParameters` - The parameter object containing the particle-size distributions. - Only implemented for the lithium-ion chemistry. + The parameter object containing the particle-size distributions + f_a_dist_n and f_a_dist_p. Returns ------- :class:`Symbol` @@ -1365,38 +1363,45 @@ def R_average(symbol, domain, param): if symbol.evaluates_on_edges("primary"): raise ValueError("Can't take the R-average of a symbol that evaluates on edges") - if domain.lower() not in ["negative", "positive"]: - raise ValueError( - """Electrode domain must be "positive" or "negative" not {}""".format( - domain.lower() - ) - ) + # If symbol doesn't have a domain, or doesn't have "negative particle size" + # or "positive particle size" as a domain, it's average value is itself + if symbol.domain == [] or not any( + domain in [["negative particle size"], ["positive particle size"]] + for domain in list(symbol.domains.values()) + ): + new_symbol = symbol.new_copy() + new_symbol.parent = None + return new_symbol - if symbol.domain not in [ - ["negative particle size"], - ["positive particle size"], + # If symbol is a primary broadcast to "particle size", take the orphan + elif isinstance(symbol, pybamm.PrimaryBroadcast) and symbol.domain in [ + ["negative particle size"], ["positive particle size"] ]: - raise pybamm.DomainError( - """R-average only implemented for primary 'particle size' domains, - but symbol has domains {}""".format( - symbol.domain - ) + return symbol.orphans[0] + # If symbol is a secondary broadcast to "particle size" from "particle", + # take the orphan + elif ( + isinstance(symbol, pybamm.SecondaryBroadcast) and + symbol.domains["secondary"] in [ + ["negative particle size"], ["positive particle size"] + ] + ): + return symbol.orphans[0] + # Otherwise, perform the integration in R + else: + R = pybamm.SpatialVariable( + "R", + domain=symbol.domain, + auxiliary_domains=symbol.auxiliary_domains, + coord_sys="cartesian", ) + if ["negative particle size"] in list(symbol.domains.values()): + f_a_dist = param.f_a_dist_n(R) + elif ["positive particle size"] in list(symbol.domains.values()): + f_a_dist = param.f_a_dist_p(R) - # Define spatial variable with same domains as symbol - R = pybamm.SpatialVariable( - "R", - domain=symbol.domain, - auxiliary_domains=symbol.auxiliary_domains, - coord_sys="cartesian", - ) - if domain.lower() == "negative": - f_a_dist = param.f_a_dist_n(R) - elif domain.lower() == "positive": - f_a_dist = param.f_a_dist_p(R) - - # enforce true average, normalising f_a_dist if it is not already - return Integral(f_a_dist * symbol, R) / Integral(f_a_dist, R) + # take average using Integral and distribution f_a_dist + return Integral(f_a_dist * symbol, R) / Integral(f_a_dist, R) def boundary_value(symbol, side): diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 14b6c4fed2..9021649b76 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -61,6 +61,30 @@ def __init__( pybamm.citations.register("Kirk2020") + def set_external_circuit_submodel(self): + """ + Define how the external circuit defines the boundary conditions for the model, + e.g. (not necessarily constant-) current, voltage, etc + """ + if self.options["operating mode"] == "current": + self.submodels["external circuit"] = pybamm.external_circuit.CurrentControl( + self.param + ) + elif self.options["operating mode"] == "voltage": + raise NotImplementedError( + """Many-Particle Model does not support voltage control.""" + ) + elif self.options["operating mode"] == "power": + self.submodels[ + "external circuit" + ] = pybamm.external_circuit.PowerFunctionControl(self.param) + elif callable(self.options["operating mode"]): + self.submodels[ + "external circuit" + ] = pybamm.external_circuit.FunctionControl( + self.param, self.options["operating mode"] + ) + def set_porosity_submodel(self): if ( @@ -199,17 +223,65 @@ def set_electrolyte_submodel(self): "electrolyte diffusion" ] = pybamm.electrolyte_diffusion.ConstantConcentration(self.param) + def set_thermal_submodel(self): + + if self.options["thermal"] == "isothermal": + thermal_submodel = pybamm.thermal.isothermal.Isothermal(self.param) + + elif self.options["thermal"] == "lumped": + thermal_submodel = pybamm.thermal.Lumped( + self.param, + cc_dimension=self.options["dimensionality"], + geometry=self.options["cell geometry"], + ) + + elif self.options["thermal"] == "x-lumped": + if self.options["dimensionality"] == 0: + # With 0D current collectors x-lumped is equivalent to lumped pouch + thermal_submodel = pybamm.thermal.Lumped(self.param, geometry="pouch") + elif self.options["dimensionality"] == 1: + thermal_submodel = pybamm.thermal.pouch_cell.CurrentCollector1D( + self.param + ) + elif self.options["dimensionality"] == 2: + thermal_submodel = pybamm.thermal.pouch_cell.CurrentCollector2D( + self.param + ) + + elif self.options["thermal"] == "x-full": + raise NotImplementedError( + """X-full thermal submodels do + not yet support particle-size distributions.""" + ) + + self.submodels["thermal"] = thermal_submodel + + def set_sei_submodel(self): + + # negative electrode SEI + if self.options["SEI"] == "none": + self.submodels["negative sei"] = pybamm.sei.NoSEI(self.param, "Negative") + else: + raise NotImplementedError( + """SEI submodels do not yet support particle-size distributions.""" + ) + + # positive electrode + self.submodels["positive sei"] = pybamm.sei.NoSEI(self.param, "Positive") + @property def default_parameter_values(self): # Default parameter values default_params = super().default_parameter_values - # Extract the particle radius, taken to be the average radius + # The mean particle radii for each electrode, taken to be the + # "Negative particle radius [m]" and "Positive particle radius [m]" + # provided in the parameter set. These will be the means of the + # (area-weighted) particle-size distributions f_a_dist_n_dim, + # f_a_dist_p_dim, provided below. R_n_dim = default_params["Negative particle radius [m]"] R_p_dim = default_params["Positive particle radius [m]"] - # Additional particle distribution parameter values - - # Area-weighted standard deviations + # Standard deviations (dimensionless) sd_a_n = 0.3 sd_a_p = 0.3 @@ -218,9 +290,9 @@ def default_parameter_values(self): R_min_p = 0 # Max radius in the particle-size distributions (dimensionless). - # Either 5 s.d.'s above the mean or 2 times the mean, whichever is larger - R_max_n = max(2, 1 + sd_a_n * 5) - R_max_p = max(2, 1 + sd_a_p * 5) + # 5 standard deviations above the mean + R_max_n = 1 + sd_a_n * 5 + R_max_p = 1 + sd_a_p * 5 # Define lognormal distribution def lognormal_distribution(R, R_av, sd): @@ -241,14 +313,14 @@ def lognormal_distribution(R, R_av, sd): / (R) ) - # Set the (area-weighted) particle-size distributions (dimensional) + # Set the dimensional (area-weighted) particle-size distributions def f_a_dist_n_dim(R): return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) def f_a_dist_p_dim(R): return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) - # Append to default parameters (dimensional) + # Append to default parameters default_params.update( { "Negative area-weighted particle-size " diff --git a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py index 0083fac88d..5adf37d609 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py @@ -7,9 +7,10 @@ class LeadingOrderSizeDistribution(BaseModel): - """An electrode submodel that employs Ohm's law the leading-order approximation to - governing equations when there is a distribution of particle sizes. An algebraic - equation is imposed for the x-averaged surface potential difference. + """An electrode submodel that employs Ohm's law the leading-order approximation + (no variation in x) to governing equations when there is a distribution of particle + sizes. An algebraic equation is imposed for the x-averaged surface potential + difference. Parameters ---------- @@ -95,6 +96,10 @@ def set_algebraic(self, variables): + self.domain.lower() + " electrode surface potential difference" ] + # Algebraic equation for the (X-avg) surface potential difference phi_s - phi_e. + # The electrode total interfacial current density (already integrated across + # particle size) must equal the sum from all sources, sum_j_av. May not account + # for interfacial current densities from reactions other than "main" self.algebraic[delta_phi_av] = sum_j_av - j_tot_av def set_initial_conditions(self, variables): @@ -115,6 +120,15 @@ def set_initial_conditions(self, variables): self.initial_conditions[delta_phi_av] = delta_phi_av_init + def set_boundary_conditions(self, variables): + + phi_s = variables[self.domain + " electrode potential"] + + lbc = (pybamm.Scalar(0), "Neumann") + rbc = (pybamm.Scalar(0), "Neumann") + + self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} + def _get_standard_surface_potential_difference_variables(self, delta_phi): if self.domain == "Negative": diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 7b7e50c3b7..d74f828b27 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -448,7 +448,7 @@ def _get_standard_exchange_current_variables(self, j0): # output exchange current density if j0.domain == [self.domain.lower() + " particle size"]: # R-average - j0 = pybamm.R_average(j0, self.domain, self.param) + j0 = pybamm.R_average(j0, self.param) # X-average, and broadcast if necessary if j0.domain == []: @@ -535,7 +535,7 @@ def _get_standard_overpotential_variables(self, eta_r): # output reaction overpotential if eta_r.domain == [self.domain.lower() + " particle size"]: # R-average - eta_r = pybamm.R_average(eta_r, self.domain, self.param) + eta_r = pybamm.R_average(eta_r, self.param) # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) @@ -650,7 +650,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # output open circuit potential if ocp.domain == [self.domain.lower() + " particle size"]: # R-average - ocp = pybamm.R_average(ocp, self.domain, self.param) + ocp = pybamm.R_average(ocp, self.param) # X-average, and broadcast if necessary if ocp.domain == []: @@ -668,7 +668,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # output entropic change if dUdT.domain == [self.domain.lower() + " particle size"]: # R-average - dUdT = pybamm.R_average(dUdT, self.domain, self.param) + dUdT = pybamm.R_average(dUdT, self.param) dUdT_av = pybamm.x_average(dUdT) @@ -731,7 +731,7 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): j_distribution = self._get_kinetics(j0, ne, eta_r, T) # R-average - j = pybamm.R_average(j_distribution, self.domain, self.param) + j = pybamm.R_average(j_distribution, self.param) return j, j_distribution def _get_standard_PSD_interfacial_current_variables(self, j_distribution): diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index 5f7e8e750e..6b8cd9711e 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -33,9 +33,6 @@ def _get_standard_concentration_distribution_variables(self, c_s): elif self.domain == "Positive": c_scale = self.param.c_p_max - # Note: Currently not possible to broadcast from (r, R) to (r, R, x) since - # domain x for broadcast is in "tertiary" position. - # Broadcast and x-average when necessary if c_s.domain == [ self.domain.lower() + " particle size" diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 41a387f24c..5dd32784cf 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -294,7 +294,7 @@ def initialise_2D(self): axis=1, ) - # Process r-x, x-z, r-R, or R-x + # Process r-x, x-z, r-R, R-x, or R-z if self.domain[0] in [ "negative particle", "positive particle", @@ -343,6 +343,14 @@ def initialise_2D(self): self.second_dimension = "x" self.R_sol = first_dim_pts self.x_sol = second_dim_pts + elif self.domain[0] in [ + "negative particle size", + "positive particle size", + ] and self.auxiliary_domains["secondary"] == ["current collector"]: + self.first_dimension = "R" + self.second_dimension = "z" + self.R_sol = first_dim_pts + self.z_sol = second_dim_pts else: raise pybamm.DomainError( "Cannot process 3D object with domain '{}' " diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py new file mode 100644 index 0000000000..122c391d36 --- /dev/null +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -0,0 +1,323 @@ +# +# Tests for the lithium-ion MPM model +# +import pybamm +import tests +import numpy as np +import unittest +from platform import system + + +class TestMPM(unittest.TestCase): + def test_basic_processing(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.MPM(options) + # use Ecker parameters for nonlinear diffusion + param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) + param = self.add_distribution_params_for_test(param) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_basic_processing_1plus1D(self): + options = {"current collector": "potential pair", "dimensionality": 1} + + model = pybamm.lithium_ion.MPM(options) + 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.R_n: 5, + var.R_p: 5, + var.y: 5, + var.z: 5, + } + modeltest = tests.StandardModelTest(model, var_pts=var_pts) + modeltest.test_all(skip_output_tests=True) + + def test_basic_processing_2plus1D(self): + options = {"current collector": "potential pair", "dimensionality": 2} + model = pybamm.lithium_ion.MPM(options) + 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.R_n: 5, + var.R_p: 5, + var.y: 5, + var.z: 5, + } + modeltest = tests.StandardModelTest(model, var_pts=var_pts) + modeltest.test_all(skip_output_tests=True) + + def test_optimisations(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.MPM(options) + optimtest = tests.OptimisationsTest(model) + + original = optimtest.evaluate_model() + using_known_evals = optimtest.evaluate_model(use_known_evals=True) + to_python = optimtest.evaluate_model(to_python=True) + np.testing.assert_array_almost_equal(original, using_known_evals) + np.testing.assert_array_almost_equal(original, to_python) + + if system() != "Windows": + to_jax = optimtest.evaluate_model(to_jax=True) + np.testing.assert_array_almost_equal(original, to_jax) + + def test_set_up(self): + model = pybamm.lithium_ion.MPM() + optimtest = tests.OptimisationsTest(model) + optimtest.set_up_model(to_python=True) + optimtest.set_up_model(to_python=False) + + def test_charge(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.MPM(options) + parameter_values = model.default_parameter_values + parameter_values.update({"Current function [A]": -1}) + modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) + modeltest.test_all() + + def test_zero_current(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.MPM(options) + parameter_values = model.default_parameter_values + parameter_values.update({"Current function [A]": 0}) + modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) + modeltest.test_all() + + def test_thermal(self): + options = {"thermal": "lumped"} + model = pybamm.lithium_ion.MPM(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + + def test_thermal_1plus1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "x-lumped" + } + model = pybamm.lithium_ion.MPM(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + + def test_particle_uniform(self): + options = {"particle": "uniform profile"} + model = pybamm.lithium_ion.MPM(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + + def test_loss_active_material(self): + options = { + "loss of active material": "none", + } + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_loss_active_material_negative(self): + options = { + "particle cracking": "no cracking", + "loss of active material": "negative", + } + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_loss_active_material_positive(self): + options = { + "particle cracking": "no cracking", + "loss of active material": "positive", + } + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_loss_active_material_both(self): + options = { + "particle cracking": "no cracking", + "loss of active material": "both", + } + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_well_posed_irreversible_plating_with_porosity(self): + options = { + "lithium plating": "irreversible", + "lithium plating porosity change": "true", + } + model = pybamm.lithium_ion.MPM(options) + param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Yang2017) + param = self.add_distribution_params_for_test(param) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def add_distribution_params_for_test(self, param): + R_n_dim = param["Negative particle radius [m]"] + R_p_dim = param["Positive particle radius [m]"] + sd_a_n = 0.3 + sd_a_p = 0.3 + + # Min and max radii + R_min_n = 0 + R_min_p = 0 + R_max_n = 1 + sd_a_n * 5 + R_max_p = 1 + sd_a_p * 5 + + def lognormal_distribution(R, R_av, sd): + import numpy as np + + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / (R) + ) + + # Set the dimensional (area-weighted) particle-size distributions + def f_a_dist_n_dim(R): + return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) + + def f_a_dist_p_dim(R): + return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) + + # Append to parameter set + param.update( + { + "Negative minimum particle radius [m]": R_min_n * R_n_dim, + "Positive minimum particle radius [m]": R_min_p * R_p_dim, + "Negative maximum particle radius [m]": R_max_n * R_n_dim, + "Positive maximum particle radius [m]": R_max_p * R_p_dim, + "Negative area-weighted " + + "particle-size distribution [m-1]": f_a_dist_n_dim, + "Positive area-weighted " + + "particle-size distribution [m-1]": f_a_dist_p_dim, + }, + check_already_exists=False, + ) + return param + + +class TestMPMWithCrack(unittest.TestCase): + def test_well_posed_none_crack(self): + options = {"particle": "Fickian diffusion", "particle cracking": "none"} + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_well_posed_no_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "no cracking"} + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_well_posed_negative_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "negative"} + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_well_posed_positive_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "positive"} + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def test_well_posed_both_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "both"} + model = pybamm.lithium_ion.MPM(options) + chemistry = pybamm.parameter_sets.Ai2020 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + param = self.add_distribution_params_for_test(parameter_values) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all() + + def add_distribution_params_for_test(self, param): + R_n_dim = param["Negative particle radius [m]"] + R_p_dim = param["Positive particle radius [m]"] + sd_a_n = 0.3 + sd_a_p = 0.3 + + # Min and max radii + R_min_n = 0 + R_min_p = 0 + R_max_n = 1 + sd_a_n * 5 + R_max_p = 1 + sd_a_p * 5 + + def lognormal_distribution(R, R_av, sd): + import numpy as np + + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / (R) + ) + + # Set the dimensional (area-weighted) particle-size distributions + def f_a_dist_n_dim(R): + return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) + + def f_a_dist_p_dim(R): + return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) + + # Append to parameter set + param.update( + { + "Negative minimum particle radius [m]": R_min_n * R_n_dim, + "Positive minimum particle radius [m]": R_min_p * R_p_dim, + "Negative maximum particle radius [m]": R_max_n * R_n_dim, + "Positive maximum particle radius [m]": R_max_p * R_p_dim, + "Negative area-weighted " + + "particle-size distribution [m-1]": f_a_dist_n_dim, + "Positive area-weighted " + + "particle-size distribution [m-1]": f_a_dist_p_dim, + }, + check_already_exists=False, + ) + return param + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + sys.setrecursionlimit(10000) + + if "-v" in sys.argv: + debug = True + unittest.main() diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 48df512d3f..9d8160ef98 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -627,6 +627,58 @@ def test_x_average(self): self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[1].id, l_p.id) + def test_R_average(self): + param = pybamm.LithiumIonParameters() + + # no domain + a = pybamm.Scalar(1) + average_a = pybamm.R_average(a, param) + self.assertEqual(average_a.id, a.id) + + b = pybamm.FullBroadcast( + 1, + ["negative particle"], + { + "secondary": "negative electrode", + "tertiary": "current collector" + } + ) + # no "particle size" domain + average_b = pybamm.R_average(b, param) + self.assertEqual(average_b.id, b.id) + + # primary or secondary broadcast to "particle size" domain + average_a = pybamm.R_average( + pybamm.PrimaryBroadcast(a, "negative particle size"), + param + ) + self.assertEqual(average_a.evaluate(), np.array([1])) + + a = pybamm.Symbol("a", domain="negative particle") + average_a = pybamm.R_average( + pybamm.SecondaryBroadcast(a, "negative particle size"), + param + ) + self.assertEqual(average_a.id, a.id) + + for domain in [["negative particle size"], ["positive particle size"]]: + a = pybamm.Symbol("a", domain=domain) + R = pybamm.SpatialVariable("R", domain) + av_a = pybamm.R_average(a, param) + self.assertIsInstance(av_a, pybamm.Division) + self.assertIsInstance(av_a.children[0], pybamm.Integral) + self.assertIsInstance(av_a.children[1], pybamm.Integral) + self.assertEqual(av_a.children[0].integration_variable[0].domain, R.domain) + # domain list should now be empty + self.assertEqual(av_a.domain, []) + + # R-average of symbol that evaluates on edges raises error + symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") + with self.assertRaisesRegex( + ValueError, "Can't take the R-average of a symbol that evaluates on edges" + ): + pybamm.R_average(symbol_on_edges, param) + def test_r_average(self): a = pybamm.Scalar(1) average_a = pybamm.r_average(a) diff --git a/tests/unit/test_geometry/test_battery_geometry.py b/tests/unit/test_geometry/test_battery_geometry.py index e2b11a064d..559914d006 100644 --- a/tests/unit/test_geometry/test_battery_geometry.py +++ b/tests/unit/test_geometry/test_battery_geometry.py @@ -8,7 +8,10 @@ class TestBatteryGeometry(unittest.TestCase): def test_geometry_keys(self): for cc_dimension in [0, 1, 2]: - geometry = pybamm.battery_geometry(current_collector_dimension=cc_dimension) + geometry = pybamm.battery_geometry( + options={"particle size": "distribution"}, + current_collector_dimension=cc_dimension + ) for domain_geoms in geometry.values(): all( self.assertIsInstance(spatial_var, pybamm.SpatialVariable) @@ -19,10 +22,14 @@ def test_geometry(self): var = pybamm.standard_spatial_vars geo = pybamm.geometric_parameters for cc_dimension in [0, 1, 2]: - geometry = pybamm.battery_geometry(current_collector_dimension=cc_dimension) + geometry = pybamm.battery_geometry( + options={"particle size": "distribution"}, + current_collector_dimension=cc_dimension + ) self.assertIsInstance(geometry, pybamm.Geometry) self.assertIn("negative electrode", geometry) self.assertIn("negative particle", geometry) + self.assertIn("negative particle size", geometry) self.assertEqual(geometry["negative electrode"][var.x_n]["min"], 0) self.assertEqual( geometry["negative electrode"][var.x_n]["max"].id, geo.l_n.id @@ -33,6 +40,9 @@ def test_geometry(self): geometry = pybamm.battery_geometry(include_particles=False) self.assertNotIn("negative particle", geometry) + geometry = pybamm.battery_geometry() + self.assertNotIn("negative particle size", geometry) + def test_geometry_error(self): with self.assertRaisesRegex(pybamm.GeometryError, "Invalid current"): pybamm.battery_geometry(current_collector_dimension=4) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py new file mode 100644 index 0000000000..f55e0410dd --- /dev/null +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -0,0 +1,273 @@ +# +# Tests for the lithium-ion MPM model +# +import pybamm +import unittest + + +class TestMPM(unittest.TestCase): + def test_well_posed(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + # Test build after init + model = pybamm.lithium_ion.MPM(build=False) + model.build_model() + model.check_well_posedness() + + def test_well_posed_2plus1D(self): + options = {"current collector": "potential pair", "dimensionality": 1} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + options = {"current collector": "potential pair", "dimensionality": 2} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_lumped_thermal_model_1D(self): + options = {"thermal": "lumped"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_x_full_thermal_not_implemented(self): + options = {"thermal": "x-full"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_x_full_Nplus1D_not_implemented(self): + # 1plus1D + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "x-full", + } + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + # 2plus1D + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "x-full", + } + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_lumped_thermal_1plus1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "lumped", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_lumped_thermal_2plus1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "lumped", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_thermal_1plus1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "x-lumped", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_thermal_2plus1D(self): + options = { + "current collector": "potential pair", + "dimensionality": 2, + "thermal": "x-lumped", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_particle_uniform(self): + options = {"particle": "uniform profile"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_particle_shape_user(self): + options = {"particle shape": "user"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_loss_active_material(self): + options = { + "loss of active material": "none", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_loss_active_material_negative(self): + options = { + "particle cracking": "no cracking", + "loss of active material": "negative", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_loss_active_material_positive(self): + options = { + "particle cracking": "no cracking", + "loss of active material": "positive", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_loss_active_material_both(self): + options = { + "particle cracking": "no cracking", + "loss of active material": "both", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_electrolyte_options(self): + options = {"electrolyte conductivity": "full"} + with self.assertRaisesRegex(pybamm.OptionError, "electrolyte conductivity"): + pybamm.lithium_ion.MPM(options) + + def test_new_model(self): + model = pybamm.lithium_ion.MPM({"thermal": "x-lumped"}) + new_model = model.new_copy() + model_T_eqn = model.rhs[model.variables["Volume-averaged cell temperature"]] + new_model_T_eqn = new_model.rhs[ + new_model.variables["Volume-averaged cell temperature"] + ] + self.assertEqual(new_model_T_eqn.id, model_T_eqn.id) + self.assertEqual(new_model.name, model.name) + self.assertEqual(new_model.use_jacobian, model.use_jacobian) + self.assertEqual(new_model.convert_to_format, model.convert_to_format) + self.assertEqual(new_model.timescale.id, model.timescale.id) + + # with custom submodels + model = pybamm.lithium_ion.MPM({"thermal": "x-lumped"}, build=False) + model.submodels[ + "negative particle" + ] = pybamm.particle.FastSingleSizeDistribution( + model.param, "Negative" + ) + model.build_model() + new_model = model.new_copy() + new_model_cs_eqn = list(new_model.rhs.values())[1] + model_cs_eqn = list(model.rhs.values())[1] + self.assertEqual(new_model_cs_eqn.id, model_cs_eqn.id) + + def test_well_posed_reversible_plating_with_porosity(self): + options = { + "lithium plating": "reversible", + "lithium plating porosity change": "true", + } + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + +class TestMPMExternalCircuits(unittest.TestCase): + def test_voltage_not_implemented(self): + options = {"operating mode": "voltage"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_well_posed_power(self): + options = {"operating mode": "power"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_function(self): + def external_circuit_function(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return V + I - pybamm.FunctionParameter("Function", {"Time [s]": pybamm.t}) + + options = {"operating mode": external_circuit_function} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + +class TestMPMWithSEI(unittest.TestCase): + def test_reaction_limited_not_implemented(self): + options = {"SEI": "reaction limited"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_solvent_diffusion_limited_not_implemented(self): + options = {"SEI": "solvent-diffusion limited"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_electron_migration_limited_not_implemented(self): + options = {"SEI": "electron-migration limited"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_interstitial_diffusion_limited_not_implemented(self): + options = {"SEI": "interstitial-diffusion limited"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + def test_ec_reaction_limited_not_implemented(self): + options = {"SEI": "ec reaction limited", "SEI porosity change": "true"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + + +class TestMPMWithCrack(unittest.TestCase): + def test_well_posed_none_crack(self): + options = {"particle": "Fickian diffusion", "particle cracking": "none"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_no_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "no cracking"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_negative_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "negative"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_positive_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "positive"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_both_cracking(self): + options = {"particle": "Fickian diffusion", "particle cracking": "both"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + +class TestMPMWithPlating(unittest.TestCase): + def test_well_posed_none_plating(self): + options = {"lithium plating": "none"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_reversible_plating(self): + options = {"lithium plating": "reversible"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + def test_well_posed_irreversible_plating(self): + options = {"lithium plating": "irreversible"} + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() + + +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_electrode/test_ohm/test_leading_size_distribution.py b/tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py new file mode 100644 index 0000000000..c91dd7615f --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py @@ -0,0 +1,52 @@ +# +# Test leading-order ohm submodel +# + +import pybamm +import tests +import unittest + + +class TestLeadingOrderSizeDistribution(unittest.TestCase): + def test_public_functions(self): + param = pybamm.LithiumIonParameters() + + a = pybamm.Scalar(0) + variables = { + "Current collector current density": a, + "Negative current collector potential": a, + "X-averaged negative" + + " electrode total interfacial current density": a, + "Sum of x-averaged negative" + + " electrode interfacial current densities": a, + } + submodel = pybamm.electrode.ohm.LeadingOrderSizeDistribution(param, "Negative") + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() + + variables = { + "Current collector current density": a, + "Negative current collector potential": a, + "Negative electrode current density": pybamm.PrimaryBroadcast( + a, ["negative electrode"] + ), + "X-averaged positive electrode surface potential difference": a, + "X-averaged positive electrolyte potential": a, + "X-averaged positive" + + " electrode total interfacial current density": a, + "Sum of x-averaged positive" + + " electrode interfacial current densities": a + } + submodel = pybamm.electrode.ohm.LeadingOrderSizeDistribution(param, "Positive") + 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() diff --git a/tests/unit/test_models/test_submodels/test_interface/test_size_distribution.py b/tests/unit/test_models/test_submodels/test_interface/test_size_distribution.py new file mode 100644 index 0000000000..568fb897e8 --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_interface/test_size_distribution.py @@ -0,0 +1,110 @@ +# +# Test interface with particle-size distributions (only implemented for lithium +# ion, so test on lithium-ion Butler-Volmer submodel) +# + +import pybamm +import tests +import unittest + + +class TestSizeDistribution(unittest.TestCase): + def test_public_functions(self): + param = pybamm.LithiumIonParameters() + + a_n = pybamm.FullBroadcast( + pybamm.Scalar(0), ["negative electrode"], "current collector" + ) + a_p = pybamm.FullBroadcast( + pybamm.Scalar(0), ["positive electrode"], "current collector" + ) + a_R_n = pybamm.Variable( + "Particle-size-dependent variable that is not a broadcast", + ["negative particle size"], + auxiliary_domains={ + "secondary": "negative electrode", + "tertiary": "current collector" + } + ) + a_R_p = pybamm.Variable( + "Particle-size-dependent variable that is not a broadcast", + ["positive particle size"], + auxiliary_domains={ + "secondary": "positive electrode", + "tertiary": "current collector" + } + ) + a = pybamm.Scalar(0) + variables = { + "Current collector current density": a, + "Negative electrode potential": a_n, + "Negative electrolyte potential": a_n, + "Negative electrode open circuit potential": a_n, + "Negative electrolyte concentration": a_n, + "Negative particle surface concentration distribution": a_R_n, + "Negative electrode temperature": a_n, + "Negative electrode surface area to volume ratio": a_n, + } + submodel = pybamm.interface.ButlerVolmer( + param, + "Negative", + "lithium-ion main", + { + "SEI film resistance": "none", + "total interfacial current density as a state": "false", + "particle size": "distribution" + }, + ) + std_tests = tests.StandardSubModelTests(submodel, variables) + + std_tests.test_all() + + variables = { + "Current collector current density": a, + "Positive electrode potential": a_p, + "Positive electrolyte potential": a_p, + "Positive electrode open circuit potential": a_p, + "Positive electrolyte concentration": a_p, + "Positive particle surface concentration distribution": a_R_p, + "Negative electrode interfacial current density": a_n, + "Negative electrode exchange current density": a_n, + "Positive electrode temperature": a_p, + "Negative electrode surface area to volume ratio": a_n, + "Positive electrode surface area to volume ratio": a_p, + "X-averaged negative electrode interfacial current density": a, + "X-averaged positive electrode interfacial current density": a, + "Sum of electrolyte reaction source terms": 0, + "Sum of negative electrode electrolyte reaction source terms": 0, + "Sum of positive electrode electrolyte reaction source terms": 0, + "Sum of x-averaged negative electrode " + "electrolyte reaction source terms": 0, + "Sum of x-averaged positive electrode " + "electrolyte reaction source terms": 0, + "Sum of interfacial current densities": 0, + "Sum of negative electrode interfacial current densities": 0, + "Sum of positive electrode interfacial current densities": 0, + "Sum of x-averaged negative electrode interfacial current densities": 0, + "Sum of x-averaged positive electrode interfacial current densities": 0, + } + submodel = pybamm.interface.ButlerVolmer( + param, + "Positive", + "lithium-ion main", + { + "SEI film resistance": "none", + "total interfacial current density as a state": "false", + "particle size": "distribution" + }, + ) + 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() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/__init__.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_base_distribution.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_base_distribution.py new file mode 100644 index 0000000000..576e2b9748 --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_base_distribution.py @@ -0,0 +1,32 @@ +# +# Test base particle size distribution submodel +# + +import pybamm +import tests +import unittest + + +class TestBaseSizeDistribution(unittest.TestCase): + def test_public_functions(self): + variables = { + "Negative particle surface concentration": 0, + "Positive particle surface concentration": 0, + } + submodel = pybamm.particle.BaseSizeDistribution(None, "Negative") + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() + + submodel = pybamm.particle.BaseSizeDistribution(None, "Positive") + 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() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py new file mode 100644 index 0000000000..2552dbe4ad --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py @@ -0,0 +1,57 @@ +# +# Test many size distributions of particles with internal uniform profile +# + +import pybamm +import tests +import unittest + + +class TestManySizeDistributions(unittest.TestCase): + def test_public_functions(self): + param = pybamm.LithiumIonParameters() + + a_n = pybamm.FullBroadcast( + pybamm.Scalar(0), "negative electrode", {"secondary": "current collector"} + ) + a_p = pybamm.FullBroadcast( + pybamm.Scalar(0), "positive electrode", {"secondary": "current collector"} + ) + + variables = { + "Negative electrode interfacial current density distribution": a_n, + "Negative electrode temperature": a_n, + "Negative electrode active material volume fraction": a_n, + "Negative electrode surface area to volume ratio": a_n, + "Negative particle radius": a_n, + } + + submodel = pybamm.particle.FastManySizeDistributions( + param, "Negative" + ) + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() + + variables = { + "Positive electrode interfacial current density distribution": a_p, + "Positive electrode temperature": a_p, + "Positive electrode active material volume fraction": a_p, + "Positive electrode surface area to volume ratio": a_p, + "Positive particle radius": a_p, + } + + submodel = pybamm.particle.FastManySizeDistributions( + param, "Positive" + ) + 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() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_single_distribution.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_single_distribution.py new file mode 100644 index 0000000000..86ea45f9db --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_single_distribution.py @@ -0,0 +1,50 @@ +# +# Test single size distribution of particles with uniform internal profile +# + +import pybamm +import tests +import unittest + + +class TestSingleSizeDistribution(unittest.TestCase): + def test_public_functions(self): + param = pybamm.LithiumIonParameters() + + a = pybamm.PrimaryBroadcast(pybamm.Scalar(0), "current collector") + + variables = { + "X-averaged negative electrode interfacial current density distribution": a, + "X-averaged negative electrode temperature": a, + "Negative electrode active material volume fraction": a, + "Negative electrode surface area to volume ratio": a, + } + + submodel = pybamm.particle.FastSingleSizeDistribution( + param, "Negative" + ) + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() + + variables = { + "X-averaged positive electrode interfacial current density distribution": a, + "X-averaged positive electrode temperature": a, + "Positive electrode active material volume fraction": a, + "Positive electrode surface area to volume ratio": a, + } + + submodel = pybamm.particle.FastSingleSizeDistribution( + param, "Positive" + ) + 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() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py new file mode 100644 index 0000000000..b00559f8f1 --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py @@ -0,0 +1,52 @@ +# +# Test many particle-size distributions with Fickian diffusion +# + +import pybamm +import tests +import unittest + + +class TestManySizeDistributions(unittest.TestCase): + def test_public_functions(self): + param = pybamm.LithiumIonParameters() + + a_n = pybamm.FullBroadcast( + pybamm.Scalar(1), "negative electrode", {"secondary": "current collector"} + ) + a_p = pybamm.FullBroadcast( + pybamm.Scalar(1), "positive electrode", {"secondary": "current collector"} + ) + + variables = { + "Negative electrode interfacial current density distribution": a_n, + "Negative electrode temperature": a_n, + "Negative electrode active material volume fraction": a_n, + "Negative electrode surface area to volume ratio": a_n, + "Negative particle radius": a_n, + } + + submodel = pybamm.particle.FickianManySizeDistributions(param, "Negative") + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() + + variables = { + "Positive electrode interfacial current density distribution": a_p, + "Positive electrode temperature": a_p, + "Positive electrode active material volume fraction": a_p, + "Positive electrode surface area to volume ratio": a_p, + "Positive particle radius": a_p, + } + submodel = pybamm.particle.FickianManySizeDistributions(param, "Positive") + 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() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_single_distribution.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_single_distribution.py new file mode 100644 index 0000000000..9699ffe6f3 --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_single_distribution.py @@ -0,0 +1,44 @@ +# +# Test single size distribution of fickian particles +# + +import pybamm +import tests +import unittest + + +class TestSingleSizeDistribution(unittest.TestCase): + def test_public_functions(self): + param = pybamm.LithiumIonParameters() + + a = pybamm.PrimaryBroadcast(pybamm.Scalar(0), "current collector") + variables = { + "X-averaged negative electrode interfacial current density distribution": a, + "X-averaged negative electrode temperature": a, + "Negative electrode active material volume fraction": a, + "Negative electrode surface area to volume ratio": a, + } + + submodel = pybamm.particle.FickianSingleSizeDistribution(param, "Negative") + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() + + variables = { + "X-averaged positive electrode interfacial current density distribution": a, + "X-averaged positive electrode temperature": a, + "Positive electrode active material volume fraction": a, + "Positive electrode surface area to volume ratio": a, + } + submodel = pybamm.particle.FickianSingleSizeDistribution(param, "Positive") + 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 b1a33f8f56e28f2e201bac32524d3c673454bc1f Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 8 Jun 2021 18:51:49 +0100 Subject: [PATCH 31/67] added standard integration tests for MPM --- .../full_battery_models/lithium_ion/mpm.py | 5 ++- .../fast_many_distributions.py | 21 +++++++++++-- .../fast_single_distribution.py | 25 ++++++++++++--- .../fickian_many_distributions.py | 19 ++++++++++-- .../fickian_single_distribution.py | 29 ++++++++++++----- .../test_models/standard_output_tests.py | 2 ++ .../test_lithium_ion/test_mpm.py | 31 +------------------ 7 files changed, 86 insertions(+), 46 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 9021649b76..4b478817e1 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -34,8 +34,11 @@ class MPM(BaseModel): def __init__( self, options=None, name="Many-Particle Model", build=True ): + if options is None: + options = {"particle size": "distribution"} + else: + options["particle size"] = "distribution" super().__init__(options, name) - self.options["particle size"] = "distribution" # Set submodels self.set_external_circuit_submodel() diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index 2997189285..0464f26c1c 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -70,6 +70,12 @@ def get_fundamental_variables(self): # or user input f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R) + # Volume-weighted particle-size distribution + f_v_dist = ( + R * f_a_dist / + pybamm.Integral(R * f_a_dist, R) + ) + # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( 0, @@ -83,8 +89,11 @@ def get_fundamental_variables(self): 0, self.domain.lower() + " electrode", "current collector" ) - # Standard R-averaged variables - c_s_surf = pybamm.Integral(f_a_dist * c_s_surf_distribution, R) + # Standard R-averaged variables. Average concentrations using + # the volume-weighted distribution since they are volume-based + # quantities. Necessary for output variables "Total lithium in + # negative electrode [mol]", etc, to be calculated correctly + c_s_surf = pybamm.Integral(f_v_dist * c_s_surf_distribution, R) c_s = pybamm.PrimaryBroadcast( c_s_surf, [self.domain.lower() + " particle"] ) @@ -108,10 +117,18 @@ def get_fundamental_variables(self): self.domain + " area-weighted particle-size" + " distribution [m-1]": pybamm.x_average(f_a_dist) / R_dim, + self.domain + " volume-weighted particle-size" + + " distribution": pybamm.x_average(f_v_dist), + self.domain + " volume-weighted particle-size" + + " distribution [m-1]": pybamm.x_average(f_v_dist) / R_dim, } ) return variables + def get_coupled_variables(self, variables): + variables.update(self._get_total_concentration_variables(variables)) + return variables + def set_rhs(self, variables): c_s_surf_distribution = variables[ self.domain diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index 3d44386c1b..0ba45fdea5 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -75,10 +75,16 @@ def get_fundamental_variables(self): # Particle-size distribution (area-weighted) f_a_dist = self.param.f_a_dist_p(R_spatial_variable) - # Ensure the distribution is normalised, irrespective of discretisation - # or user input + # Ensure the area-weighted distribution is normalised, irrespective + # of discretisation or user input f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_spatial_variable) + # Volume-weighted particle-size distribution + f_v_dist = ( + R_spatial_variable * f_a_dist / + pybamm.Integral(R_spatial_variable * f_a_dist, R_spatial_variable) + ) + # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( 0, @@ -92,9 +98,12 @@ def get_fundamental_variables(self): 0, self.domain.lower() + " electrode", "current collector" ) - # Standard R-averaged variables + # Standard R-averaged variables. Average concentrations using + # the volume-weighted distribution since they are volume-based + # quantities. Necessary for output variables "Total lithium in + # negative electrode [mol]", etc, to be calculated correctly c_s_surf_xav = pybamm.Integral( - f_a_dist * c_s_surf_xav_distribution, R_spatial_variable + f_v_dist * c_s_surf_xav_distribution, R_spatial_variable ) c_s_xav = pybamm.PrimaryBroadcast( c_s_surf_xav, [self.domain.lower() + " particle"] @@ -119,10 +128,18 @@ def get_fundamental_variables(self): self.domain + " area-weighted particle-size" + " distribution [m-1]": f_a_dist / R_dim, + self.domain + " volume-weighted particle-size" + + " distribution": f_v_dist, + self.domain + " volume-weighted particle-size" + + " distribution [m-1]": f_v_dist / R_dim, } ) return variables + def get_coupled_variables(self, variables): + variables.update(self._get_total_concentration_variables(variables)) + return variables + def set_rhs(self, variables): c_s_surf_xav_distribution = variables[ "X-averaged " diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py index 320c8621cc..ec12dfb7e0 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py @@ -89,8 +89,17 @@ def get_fundamental_variables(self): # or user input f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) - # Standard R-averaged variables (avg secondary domain) - c_s = pybamm.Integral(f_a_dist * c_s_distribution, R_variable) + # Volume-weighted particle-size distribution + f_v_dist = ( + R_variable * f_a_dist / + pybamm.Integral(R_variable * f_a_dist, R_variable) + ) + + # Standard R-averaged variables. Average concentrations using + # the volume-weighted distribution since they are volume-based + # quantities. Necessary for output variables "Total lithium in + # negative electrode [mol]", etc, to be calculated correctly + c_s = pybamm.Integral(f_v_dist * c_s_distribution, R_variable) c_s_xav = pybamm.x_average(c_s) variables = self._get_standard_concentration_variables(c_s, c_s_xav) @@ -108,6 +117,10 @@ def get_fundamental_variables(self): self.domain + " area-weighted particle-size" + " distribution [m-1]": pybamm.x_average(f_a_dist) / R_dim, + self.domain + " volume-weighted particle-size" + + " distribution": pybamm.x_average(f_v_dist), + self.domain + " volume-weighted particle-size" + + " distribution [m-1]": pybamm.x_average(f_v_dist) / R_dim, } ) return variables @@ -174,6 +187,8 @@ def get_coupled_variables(self, variables): variables.update( {self.domain + " particle flux distribution": N_s_distribution} ) + + variables.update(self._get_total_concentration_variables(variables)) return variables def set_rhs(self, variables): diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index e416f7d94f..2b14f7ac48 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -81,8 +81,17 @@ def get_fundamental_variables(self): # or user input f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_spatial_variable) - # Standard R-averaged variables (avg secondary domain) - c_s_xav = pybamm.Integral(f_a_dist * c_s_xav_distribution, R_spatial_variable) + # Volume-weighted particle-size distribution + f_v_dist = ( + R_spatial_variable * f_a_dist / + pybamm.Integral(R_spatial_variable * f_a_dist, R_spatial_variable) + ) + + # Standard R-averaged variables. Average concentrations using + # the volume-weighted distribution since they are volume-based + # quantities. Necessary for output variables "Total lithium in + # negative electrode [mol]", etc, to be calculated correctly + c_s_xav = pybamm.Integral(f_v_dist * c_s_xav_distribution, R_spatial_variable) c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) variables = self._get_standard_concentration_variables(c_s, c_s_xav) @@ -100,6 +109,10 @@ def get_fundamental_variables(self): + " distribution": f_a_dist, self.domain + " area-weighted particle-size" + " distribution [m-1]": f_a_dist / R_dim, + self.domain + " volume-weighted particle-size" + + " distribution": f_v_dist, + self.domain + " volume-weighted particle-size" + + " distribution [m-1]": f_v_dist / R_dim, } ) return variables @@ -109,7 +122,6 @@ def get_coupled_variables(self, variables): "X-averaged " + self.domain.lower() + " particle concentration distribution" ] R_spatial_variable = variables[self.domain + " particle size"] - f_a_dist = variables[self.domain + " area-weighted particle-size distribution"] # broadcast to "particle size" domain then again into "particle" T_k_xav = pybamm.PrimaryBroadcast( @@ -120,9 +132,6 @@ def get_coupled_variables(self, variables): R = pybamm.PrimaryBroadcast( R_spatial_variable, [self.domain.lower() + " particle"], ) - f_a_dist = pybamm.PrimaryBroadcast( - f_a_dist, [self.domain.lower() + " particle"], - ) if self.domain == "Negative": N_s_xav_distribution = -self.param.D_n( @@ -133,7 +142,12 @@ def get_coupled_variables(self, variables): c_s_xav_distribution, T_k_xav ) * pybamm.grad(c_s_xav_distribution) / R - # Standard R-averaged flux variables + # Standard R-averaged flux variables. Average using the area-weighted + # distribution + f_a_dist = variables[self.domain + " area-weighted particle-size distribution"] + f_a_dist = pybamm.PrimaryBroadcast( + f_a_dist, [self.domain.lower() + " particle"], + ) N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_spatial_variable) N_s = pybamm.SecondaryBroadcast(N_s_xav, [self.domain.lower() + " electrode"]) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) @@ -147,6 +161,7 @@ def get_coupled_variables(self, variables): + " particle flux distribution": N_s_xav_distribution, } ) + variables.update(self._get_total_concentration_variables(variables)) return variables def set_rhs(self, variables): diff --git a/tests/integration/test_models/standard_output_tests.py b/tests/integration/test_models/standard_output_tests.py index 166bee579d..df40d75548 100644 --- a/tests/integration/test_models/standard_output_tests.py +++ b/tests/integration/test_models/standard_output_tests.py @@ -335,6 +335,8 @@ def test_conservation(self): diff = (self.c_s_tot[1:] - self.c_s_tot[:-1]) / self.c_s_tot[:-1] if "profile" in self.model.options["particle"]: np.testing.assert_array_almost_equal(diff, 0, decimal=10) + elif self.model.options["particle size"] == "distribution": + np.testing.assert_array_almost_equal(diff, 0, decimal=10) elif self.model.options["surface form"] == "differential": np.testing.assert_array_almost_equal(diff, 0, decimal=10) elif self.model.options["SEI"] == "ec reaction limited": diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 122c391d36..b81641bc33 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -76,14 +76,6 @@ def test_set_up(self): optimtest.set_up_model(to_python=True) optimtest.set_up_model(to_python=False) - def test_charge(self): - options = {"thermal": "isothermal"} - model = pybamm.lithium_ion.MPM(options) - parameter_values = model.default_parameter_values - parameter_values.update({"Current function [A]": -1}) - modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) - modeltest.test_all() - def test_zero_current(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) @@ -92,22 +84,12 @@ def test_zero_current(self): modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() - def test_thermal(self): + def test_thermal_lumped(self): options = {"thermal": "lumped"} model = pybamm.lithium_ion.MPM(options) modeltest = tests.StandardModelTest(model) modeltest.test_all() - def test_thermal_1plus1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 1, - "thermal": "x-lumped" - } - model = pybamm.lithium_ion.MPM(options) - modeltest = tests.StandardModelTest(model) - modeltest.test_all() - def test_particle_uniform(self): options = {"particle": "uniform profile"} model = pybamm.lithium_ion.MPM(options) @@ -161,17 +143,6 @@ def test_loss_active_material_both(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - def test_well_posed_irreversible_plating_with_porosity(self): - options = { - "lithium plating": "irreversible", - "lithium plating porosity change": "true", - } - model = pybamm.lithium_ion.MPM(options) - param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Yang2017) - param = self.add_distribution_params_for_test(param) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - def add_distribution_params_for_test(self, param): R_n_dim = param["Negative particle radius [m]"] R_p_dim = param["Positive particle radius [m]"] From df131412a3c6744f0d745236bfd7e848bce65960 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 10 Jun 2021 13:41:27 +0100 Subject: [PATCH 32/67] change MPM submodels to surface form --- .../full_battery_models/lithium_ion/mpm.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 4b478817e1..4fa37af28f 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -34,10 +34,15 @@ class MPM(BaseModel): def __init__( self, options=None, name="Many-Particle Model", build=True ): + # Necessary options if options is None: - options = {"particle size": "distribution"} + options = { + "particle size": "distribution", + "surface form": "algebraic" + } else: options["particle size"] = "distribution" + options["surface form"] = "algebraic" super().__init__(options, name) # Set submodels @@ -186,13 +191,13 @@ def set_negative_electrode_submodel(self): self.submodels[ "negative electrode potential" - ] = pybamm.electrode.ohm.LeadingOrderSizeDistribution(self.param, "Negative") + ] = pybamm.electrode.ohm.LeadingOrder(self.param, "Negative") def set_positive_electrode_submodel(self): self.submodels[ "positive electrode potential" - ] = pybamm.electrode.ohm.LeadingOrderSizeDistribution(self.param, "Positive") + ] = pybamm.electrode.ohm.LeadingOrder(self.param, "Positive") def set_electrolyte_submodel(self): @@ -200,23 +205,17 @@ def set_electrolyte_submodel(self): if self.options["electrolyte conductivity"] not in ["default", "leading order"]: raise pybamm.OptionError( - "electrolyte conductivity '{}' not suitable for SPM".format( + "electrolyte conductivity '{}' not suitable for MPM".format( self.options["electrolyte conductivity"] ) ) - - if self.options["surface form"] == "false": - self.submodels[ - "leading-order electrolyte conductivity" - ] = pybamm.electrolyte_conductivity.LeadingOrder(self.param) - - elif self.options["surface form"] == "differential": - for domain in ["Negative", "Separator", "Positive"]: - self.submodels[ - "leading-order " + domain.lower() + " electrolyte conductivity" - ] = surf_form.LeadingOrderDifferential(self.param, domain) - - elif self.options["surface form"] == "algebraic": + if self.options["surface form"] != "algebraic": + raise pybamm.OptionError( + "surface form must be 'algebraic' not '{}' for MPM".format( + self.options["electrolyte conductivity"] + ) + ) + else: for domain in ["Negative", "Separator", "Positive"]: self.submodels[ "leading-order " + domain.lower() + " electrolyte conductivity" @@ -274,8 +273,8 @@ def set_sei_submodel(self): @property def default_parameter_values(self): - # Default parameter values default_params = super().default_parameter_values + # The mean particle radii for each electrode, taken to be the # "Negative particle radius [m]" and "Positive particle radius [m]" # provided in the parameter set. These will be the means of the From 415ee98a39b14979876746c97a8d1c44352f9c08 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 10 Jun 2021 13:44:04 +0100 Subject: [PATCH 33/67] delete plots_of_MPM script --- .../plots_of_PSDModel.py | 135 ------------------ 1 file changed, 135 deletions(-) delete mode 100644 add-PSD-scripts-and-notebooks/plots_of_PSDModel.py diff --git a/add-PSD-scripts-and-notebooks/plots_of_PSDModel.py b/add-PSD-scripts-and-notebooks/plots_of_PSDModel.py deleted file mode 100644 index 47d687b15e..0000000000 --- a/add-PSD-scripts-and-notebooks/plots_of_PSDModel.py +++ /dev/null @@ -1,135 +0,0 @@ -import pybamm -import matplotlib.pyplot as plt - - -pybamm.set_logging_level("DEBUG") - -# Experiment -# (Use default 1C discharge from full) -t_eval = [0, 3600] - -# Parameter values -params = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019) -R_n_dim = params["Negative particle radius [m]"] -R_p_dim = params["Positive particle radius [m]"] - -# Add distribution parameters - -# Standard deviations -sd_a_n = 0.3 # dimensionless -sd_a_p = 0.3 -sd_a_n_dim = sd_a_n * R_n_dim # dimensional -sd_a_p_dim = sd_a_p * R_p_dim - -# Minimum and maximum particle sizes (dimensionaless) -R_min_n = 0 -R_min_p = 0 -R_max_n = max(2, 1 + sd_a_n * 5) -R_max_p = max(2, 1 + sd_a_p * 5) - - -def lognormal_distribution(R, R_av, sd): - ''' - A lognormal distribution with arguments - R : particle radius - R_av: mean particle radius - sd : standard deviation - ''' - import numpy as np - - # calculate usual lognormal parameters - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / R - ) - - -# Set the dimensional (area-weighted) particle-size distributions -# Note: the only argument must be the particle size R -def f_a_dist_n_dim(R): - return lognormal_distribution(R, R_n_dim, sd_a_n_dim) - - -def f_a_dist_p_dim(R): - return lognormal_distribution(R, R_p_dim, sd_a_p_dim) - - -# input distribution params (dimensional) -distribution_params = { - "Negative area-weighted particle-size " - + "standard deviation [m]": sd_a_n_dim, - "Positive area-weighted particle-size " - + "standard deviation [m]": sd_a_p_dim, - "Negative minimum particle radius [m]": R_min_n * R_n_dim, - "Positive minimum particle radius [m]": R_min_p * R_p_dim, - "Negative maximum particle radius [m]": R_max_n * R_n_dim, - "Positive maximum particle radius [m]": R_max_p * R_p_dim, - "Negative area-weighted " - + "particle-size distribution [m-1]": f_a_dist_n_dim, - "Positive area-weighted " - + "particle-size distribution [m-1]": f_a_dist_p_dim, -} -params.update(distribution_params, check_already_exists=False) - -# MPM -model_1 = pybamm.lithium_ion.MPM(name="MPM") # default params - -# DFN with PSD option -model_2 = pybamm.lithium_ion.DFN( - #options={"particle-size distribution": "true"}, - #name="MP-DFN" -) - -# DFN (no particle-size distributions) -model_3 = pybamm.lithium_ion.DFN(name="DFN") - -models = [model_1, model_2, model_3] - -sims=[] -for model in models: - sim = pybamm.Simulation( - model, - parameter_values=params, - solver=pybamm.CasadiSolver(mode="fast") - ) - sims.append(sim) - -# Reduce number of points in R -var = pybamm.standard_spatial_vars -sims[1].var_pts.update( - { - var.R_n: 20, - var.R_p: 20, - } -) - -# Solve -for sim in sims: - sim.solve(t_eval=t_eval) - - -# Plot -output_variables = [ - "Negative particle surface concentration", - "Positive particle surface concentration", - "X-averaged negative particle surface concentration distribution", - "X-averaged positive particle surface concentration distribution", -# "Negative particle surface concentration distribution", -# "Positive particle surface concentration distribution", - "Negative area-weighted particle-size distribution", - "Positive area-weighted particle-size distribution", - "Terminal voltage [V]", -] -# MPM -sims[0].plot(output_variables) -# MPM and MP-DFN -#pybamm.dynamic_plot([sims[0], sims[1]], output_variables=output_variables) -#pybamm.dynamic_plot(sims[1], output_variables=[ -# "Negative particle surface concentration distribution", -# "Positive particle surface concentration distribution", -#]) -# MPM, MP-DFN and DFN -pybamm.dynamic_plot(sims) From a6aa4776e2249e799089a9f0e1ebdcea9761f514 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 10 Jun 2021 14:00:19 +0100 Subject: [PATCH 34/67] remove test for Fickian MPDFN --- .../test_fickian_many_distributions.py | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py deleted file mode 100644 index b00559f8f1..0000000000 --- a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fickian_many_distributions.py +++ /dev/null @@ -1,52 +0,0 @@ -# -# Test many particle-size distributions with Fickian diffusion -# - -import pybamm -import tests -import unittest - - -class TestManySizeDistributions(unittest.TestCase): - def test_public_functions(self): - param = pybamm.LithiumIonParameters() - - a_n = pybamm.FullBroadcast( - pybamm.Scalar(1), "negative electrode", {"secondary": "current collector"} - ) - a_p = pybamm.FullBroadcast( - pybamm.Scalar(1), "positive electrode", {"secondary": "current collector"} - ) - - variables = { - "Negative electrode interfacial current density distribution": a_n, - "Negative electrode temperature": a_n, - "Negative electrode active material volume fraction": a_n, - "Negative electrode surface area to volume ratio": a_n, - "Negative particle radius": a_n, - } - - submodel = pybamm.particle.FickianManySizeDistributions(param, "Negative") - std_tests = tests.StandardSubModelTests(submodel, variables) - std_tests.test_all() - - variables = { - "Positive electrode interfacial current density distribution": a_p, - "Positive electrode temperature": a_p, - "Positive electrode active material volume fraction": a_p, - "Positive electrode surface area to volume ratio": a_p, - "Positive particle radius": a_p, - } - submodel = pybamm.particle.FickianManySizeDistributions(param, "Positive") - 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 ea72822b8cddd5f67c5f6be0c93eaf7a2870fda1 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 11 Jun 2021 15:43:29 +0100 Subject: [PATCH 35/67] fixed surface area for MP models and tests --- .../active_material/base_active_material.py | 27 +++- .../leading_surface_form_conductivity.py | 12 +- .../submodels/interface/base_interface.py | 2 +- .../size_distribution/base_distribution.py | 127 ++++++++++++++- .../fast_many_distributions.py | 59 ++----- .../fast_single_distribution.py | 64 +++----- .../fickian_many_distributions.py | 101 +++--------- .../fickian_single_distribution.py | 72 ++++----- .../test_models/standard_output_tests.py | 60 ++++++- .../test_lithium_ion/test_compare_outputs.py | 82 ++++++++++ .../test_lithium_ion/test_mpm.py | 152 +++++------------- 11 files changed, 418 insertions(+), 340 deletions(-) diff --git a/pybamm/models/submodels/active_material/base_active_material.py b/pybamm/models/submodels/active_material/base_active_material.py index 1c94af7e60..f43aa330d8 100644 --- a/pybamm/models/submodels/active_material/base_active_material.py +++ b/pybamm/models/submodels/active_material/base_active_material.py @@ -65,16 +65,31 @@ def _get_standard_active_material_variables(self, eps_solid): C = eps_solid_av * L * param.A_cc * c_s_max * param.F / 3600 variables.update({self.domain + " electrode capacity [A.h]": C}) + # If a single particle size at every x, use the parameters + # R_n, R_p. For a size distribution, calculate the area-weighted + # mean using the distribution instead. Then the surface area is + # calculated the same way if self.domain == "Negative": - x = pybamm.standard_spatial_vars.x_n - R = self.param.R_n(x) - R_dim = self.param.R_n_dimensional(x * self.param.L_x) + if self.options["particle size"] == "single": + x = pybamm.standard_spatial_vars.x_n + R = self.param.R_n(x) + R_dim = self.param.R_n_dimensional(x * self.param.L_x) + elif self.options["particle size"] == "distribution": + R_n = pybamm.standard_spatial_vars.R_n + R = pybamm.R_average(R_n, self.param) + R_dim = R * self.param.R_n_typ a_typ = self.param.a_n_typ elif self.domain == "Positive": - x = pybamm.standard_spatial_vars.x_p - R = self.param.R_p(x) - R_dim = self.param.R_p_dimensional(x * self.param.L_x) + if self.options["particle size"] == "single": + x = pybamm.standard_spatial_vars.x_p + R = self.param.R_p(x) + R_dim = self.param.R_p_dimensional(x * self.param.L_x) + elif self.options["particle size"] == "distribution": + R_p = pybamm.standard_spatial_vars.R_p + R = pybamm.R_average(R_p, self.param) + R_dim = R * self.param.R_p_typ a_typ = self.param.a_p_typ + R_dim_av = pybamm.x_average(R_dim) # Compute dimensional particle shape diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py b/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py index 5353ace718..a2e0d3e204 100644 --- a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py +++ b/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py @@ -149,6 +149,16 @@ def set_algebraic(self, variables): if self.domain == "Separator": return + # Get x-averaged surface area to volume ratio. It should be 1 for + # the x-average models with a single particle size (SPM, SPMe). But for + # x-averaged models with a particle size distribution (MPM) it is not + # equal to 1 since it was scaled by a_typ, which is likely not the surface area + # of the final (discretized) distribution. + a = variables[ + "X-averaged " + self.domain.lower() + + " electrode surface area to volume ratio" + ] + sum_j = variables[ "Sum of x-averaged " + self.domain.lower() @@ -166,4 +176,4 @@ def set_algebraic(self, variables): + " electrode surface potential difference" ] - self.algebraic[delta_phi] = sum_j_av - sum_j + self.algebraic[delta_phi] = sum_j_av - a * sum_j diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index d74f828b27..4996f0f1dc 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -715,7 +715,7 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): """ Calculates current density (j_distribution) that depends on particle size for "particle-size distribution" models, and - the standard R-averaged current density (j) + the R-averaged (using area-weighted distribution) current density (j) """ # T must have same domains as j0, eta_r, so remove electrode domain from T # if necessary (only check eta_r, as j0 should already match) diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index 6b8cd9711e..9967c5adb8 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -23,6 +23,106 @@ class BaseSizeDistribution(BaseParticle): def __init__(self, param, domain): super().__init__(param, domain) + def _get_distribution_variables(self, R): + """ + Forms the particle-size distributions and mean radii given a spatial variable + R. The domains of R will be different depending on the submodel, e.g. for the + `SingleSizeDistribution` classes R does not have an "electrode" domain. + """ + if self.domain == "Negative": + R_typ = self.param.R_n_typ + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_n(R) + elif self.domain == "Positive": + R_typ = self.param.R_p_typ + # Particle-size distribution (area-weighted) + f_a_dist = self.param.f_a_dist_p(R) + + # Ensure the distribution is normalised, irrespective of discretisation + # or user input + f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R) + + # Volume-weighted particle-size distribution + f_v_dist = R * f_a_dist / pybamm.Integral(R * f_a_dist, R) + + # Number-based particle-size distribution + f_num_dist = (f_a_dist / R ** 2) / pybamm.Integral(f_a_dist / R ** 2, R) + + # True mean radii, given the f_a_dist that was given + true_R_num_mean = pybamm.Integral(R * f_num_dist, R) + true_R_a_mean = pybamm.Integral(R * f_a_dist, R) + true_R_v_mean = pybamm.Integral(R * f_v_dist, R) + + # X-average the mean radii (to remove the "electrode" domain, if present) + true_R_num_mean = pybamm.x_average(true_R_num_mean) + true_R_a_mean = pybamm.x_average(true_R_a_mean) + true_R_v_mean = pybamm.x_average(true_R_v_mean) + + # X-averaged distributions + if R.auxiliary_domains["secondary"] == [self.domain.lower() + " electrode"]: + f_a_dist_xav = pybamm.x_average(f_a_dist) + f_v_dist_xav = pybamm.x_average(f_v_dist) + f_num_dist_xav = pybamm.x_average(f_num_dist) + else: + f_a_dist_xav = f_a_dist + f_v_dist_xav = f_v_dist + f_num_dist_xav = f_num_dist + + # broadcast + f_a_dist = pybamm.SecondaryBroadcast( + f_a_dist_xav, [self.domain.lower() + " electrode"] + ) + f_v_dist = pybamm.SecondaryBroadcast( + f_v_dist_xav, [self.domain.lower() + " electrode"] + ) + f_num_dist = pybamm.SecondaryBroadcast( + f_num_dist_xav, [self.domain.lower() + " electrode"] + ) + + variables = { + self.domain + " particle sizes": R, + self.domain + " particle sizes [m]": R * R_typ, + self.domain + " area-weighted particle-size" + + " distribution": f_a_dist, + self.domain + " area-weighted particle-size" + + " distribution [m-1]": f_a_dist / R_typ, + self.domain + " volume-weighted particle-size" + + " distribution": f_v_dist, + self.domain + " volume-weighted particle-size" + + " distribution [m-1]": f_v_dist / R_typ, + self.domain + " number-based particle-size" + + " distribution": f_num_dist, + self.domain + " number-based particle-size" + + " distribution [m-1]": f_num_dist / R_typ, + "True " + self.domain.lower() + " area-weighted" + + " mean radius": true_R_a_mean, + "True " + self.domain.lower() + " area-weighted" + + " mean radius [m]": true_R_a_mean * R_typ, + "True " + self.domain.lower() + " volume-weighted" + + " mean radius": true_R_v_mean, + "True " + self.domain.lower() + " volume-weighted" + + " mean radius [m]": true_R_v_mean * R_typ, + "True " + self.domain.lower() + " number-based" + + " mean radius": true_R_num_mean, + "True " + self.domain.lower() + " number-based" + + " mean radius [m]": true_R_num_mean * R_typ, + # X-averaged distributions + "X-averaged " + self.domain.lower() + + " area-weighted particle-size distribution": f_a_dist_xav, + "X-averaged " + self.domain.lower() + + " area-weighted particle-size distribution [m-1]": f_a_dist_xav / R_typ, + "X-averaged " + self.domain.lower() + + " volume-weighted particle-size distribution": f_v_dist_xav, + "X-averaged " + self.domain.lower() + + " volume-weighted particle-size distribution [m-1]": f_v_dist_xav / R_typ, + "X-averaged " + self.domain.lower() + + " number-based particle-size distribution": f_num_dist_xav, + "X-averaged " + self.domain.lower() + + " number-based particle-size distribution [m-1]": f_num_dist_xav / R_typ, + } + + return variables + def _get_standard_concentration_distribution_variables(self, c_s): """ Forms standard concentration variables that depend on particle size R given @@ -103,8 +203,6 @@ def _get_standard_concentration_distribution_variables(self, c_s): c_s_distribution = c_s # x-average the *tertiary* domain. Do manually using Integral - #x = pybamm.standard_spatial_vars.x_p - #l = pybamm.geometric_parameters.l_p x = pybamm.SpatialVariable("x", domain=[self.domain.lower() + " electrode"]) v = pybamm.ones_like(c_s) l = pybamm.Integral(v, x) @@ -143,3 +241,28 @@ def _get_standard_concentration_distribution_variables(self, c_s): + " distribution [mol.m-3]": c_scale * c_s_surf_distribution, } return variables + + def _get_surface_area_output_variables(self, variables): + + if self.domain == "Negative": + a_typ = self.param.a_n_typ + elif self.domain == "Positive": + a_typ = self.param.a_p_typ + + true_R_a_mean = variables[ + "True " + self.domain.lower() + " area-weighted mean radius" + ] + + # True surface area to volume ratio, using the true area-weighted mean + # radius calculated from the distribution + true_a = 1 / true_R_a_mean + + variables.update( + { + "True " + self.domain.lower() + " electrode surface area to volume" + + " ratio" : true_a, + "True " + self.domain.lower() + " electrode surface area to volume" + + " ratio [m-1]" : true_a * a_typ, + } + ) + return variables diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index 0464f26c1c..7bdcc64071 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -26,7 +26,7 @@ class FastManySizeDistributions(BaseSizeDistribution): def __init__(self, param, domain): super().__init__(param, domain) - # pybamm.citations.register("kirk2020") + pybamm.citations.register("Kirk2020") def get_fundamental_variables(self): # The concentration is uniform throughout each particle, so we @@ -44,10 +44,6 @@ def get_fundamental_variables(self): bounds=(0, 1), ) R = pybamm.standard_spatial_vars.R_n - R_dim = self.param.R_n_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R) elif self.domain == "Positive": # distribution variables @@ -61,20 +57,9 @@ def get_fundamental_variables(self): bounds=(0, 1), ) R = pybamm.standard_spatial_vars.R_p - R_dim = self.param.R_p_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R) - # Ensure the distribution is normalised, irrespective of discretisation - # or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R) - - # Volume-weighted particle-size distribution - f_v_dist = ( - R * f_a_dist / - pybamm.Integral(R * f_a_dist, R) - ) + # Distribution variables + variables = self._get_distribution_variables(R) # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( @@ -89,44 +74,32 @@ def get_fundamental_variables(self): 0, self.domain.lower() + " electrode", "current collector" ) + # Standard concentration distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables( + c_s_surf_distribution + ) + ) + # Standard R-averaged variables. Average concentrations using # the volume-weighted distribution since they are volume-based # quantities. Necessary for output variables "Total lithium in # negative electrode [mol]", etc, to be calculated correctly + f_v_dist = variables[ + self.domain + " volume-weighted particle-size distribution" + ] c_s_surf = pybamm.Integral(f_v_dist * c_s_surf_distribution, R) c_s = pybamm.PrimaryBroadcast( c_s_surf, [self.domain.lower() + " particle"] ) c_s_xav = pybamm.x_average(c_s) - variables = self._get_standard_concentration_variables(c_s, c_s_xav) + variables.update(self._get_standard_concentration_variables(c_s, c_s_xav)) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) - - # Standard distribution variables (R-dependent) - variables.update( - self._get_standard_concentration_distribution_variables( - c_s_surf_distribution - ) - ) - variables.update( - { - self.domain + " particle size": R, - self.domain + " particle size [m]": R * R_dim, - self.domain - + " area-weighted particle-size" - + " distribution": pybamm.x_average(f_a_dist), - self.domain - + " area-weighted particle-size" - + " distribution [m-1]": pybamm.x_average(f_a_dist) / R_dim, - self.domain + " volume-weighted particle-size" - + " distribution": pybamm.x_average(f_v_dist), - self.domain + " volume-weighted particle-size" - + " distribution [m-1]": pybamm.x_average(f_v_dist) / R_dim, - } - ) return variables def get_coupled_variables(self, variables): variables.update(self._get_total_concentration_variables(variables)) + variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): @@ -138,7 +111,7 @@ def set_rhs(self, variables): self.domain + " electrode interfacial current density distribution" ] - R = variables[self.domain + " particle size"] + R = variables[self.domain + " particle sizes"] if self.domain == "Negative": self.rhs = { diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index 0ba45fdea5..672e1e0362 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -42,16 +42,12 @@ def get_fundamental_variables(self): # Since concentration does not depend on "x", need a particle-size # spatial variable R with only "current collector" as secondary # domain - R_spatial_variable = pybamm.SpatialVariable( + R = pybamm.SpatialVariable( "R_n", domain=["negative particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) - R_dim = self.param.R_n_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_spatial_variable) elif self.domain == "Positive": # distribution variables @@ -64,26 +60,15 @@ def get_fundamental_variables(self): # Since concentration does not depend on "x", need a particle-size # spatial variable R with only "current collector" as secondary # domain - R_spatial_variable = pybamm.SpatialVariable( + R = pybamm.SpatialVariable( "R_p", domain=["positive particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) - R_dim = self.param.R_p_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_spatial_variable) - - # Ensure the area-weighted distribution is normalised, irrespective - # of discretisation or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_spatial_variable) - # Volume-weighted particle-size distribution - f_v_dist = ( - R_spatial_variable * f_a_dist / - pybamm.Integral(R_spatial_variable * f_a_dist, R_spatial_variable) - ) + # Distribution variables + variables = self._get_distribution_variables(R) # Flux variables (zero) N_s = pybamm.FullBroadcastToEdges( @@ -98,46 +83,35 @@ def get_fundamental_variables(self): 0, self.domain.lower() + " electrode", "current collector" ) + # Standard distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables( + c_s_surf_xav_distribution + ) + ) + # Standard R-averaged variables. Average concentrations using # the volume-weighted distribution since they are volume-based # quantities. Necessary for output variables "Total lithium in # negative electrode [mol]", etc, to be calculated correctly + f_v_dist = variables[ + "X-averaged " + self.domain.lower() + + " volume-weighted particle-size distribution" + ] c_s_surf_xav = pybamm.Integral( - f_v_dist * c_s_surf_xav_distribution, R_spatial_variable + f_v_dist * c_s_surf_xav_distribution, R ) c_s_xav = pybamm.PrimaryBroadcast( c_s_surf_xav, [self.domain.lower() + " particle"] ) c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) - variables = self._get_standard_concentration_variables(c_s, c_s_xav) + variables.update(self._get_standard_concentration_variables(c_s, c_s_xav)) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) - - # Standard distribution variables (R-dependent) - variables.update( - self._get_standard_concentration_distribution_variables( - c_s_surf_xav_distribution - ) - ) - variables.update( - { - self.domain + " particle size": R_spatial_variable, - self.domain + " particle size [m]": R_spatial_variable * R_dim, - self.domain - + " area-weighted particle-size" - + " distribution": f_a_dist, - self.domain - + " area-weighted particle-size" - + " distribution [m-1]": f_a_dist / R_dim, - self.domain + " volume-weighted particle-size" - + " distribution": f_v_dist, - self.domain + " volume-weighted particle-size" - + " distribution [m-1]": f_v_dist / R_dim, - } - ) return variables def get_coupled_variables(self, variables): variables.update(self._get_total_concentration_variables(variables)) + variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): @@ -151,7 +125,7 @@ def set_rhs(self, variables): + self.domain.lower() + " electrode interfacial current density distribution" ] - R = variables[self.domain + " particle size"] + R = variables[self.domain + " particle sizes"] if self.domain == "Negative": self.rhs = { diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py index ec12dfb7e0..e3f8d74161 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py @@ -25,7 +25,7 @@ class FickianManySizeDistributions(BaseSizeDistribution): def __init__(self, param, domain): super().__init__(param, domain) - # pybamm.citations.register("kirk2020") + pybamm.citations.register("Kirk2020") def get_fundamental_variables(self): if self.domain == "Negative": @@ -39,22 +39,7 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - # Since concentration does not depend on "y,z", need a particle-size - # spatial variable R with only "electrode" as secondary - # domain - R_variable = pybamm.SpatialVariable( - "R_n", - domain=["negative particle size"], - auxiliary_domains={ - "secondary": "negative electrode", - }, - coord_sys="cartesian", - ) - #R_variable = pybamm.standard_spatial_vars.R_n - R_dim = self.param.R_n_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_variable) + R_variable = pybamm.standard_spatial_vars.R_n elif self.domain == "Positive": # distribution variables @@ -67,70 +52,37 @@ def get_fundamental_variables(self): }, bounds=(0, 1), ) - # Since concentration does not depend on "y,z", need a - # spatial variable R with only "electrode" as secondary - # domain - R_variable = pybamm.SpatialVariable( - "R_p", - domain=["positive particle size"], - auxiliary_domains={ - "secondary": "positive electrode", - }, - coord_sys="cartesian", - ) - #R = pybamm.standard_spatial_vars.R_p # used for averaging - #R_variable = pybamm.SecondaryBroadcast(R, ["positive electrode"]) - R_dim = self.param.R_p_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_variable) + R = pybamm.standard_spatial_vars.R_p - # Ensure the distribution is normalised, irrespective of discretisation - # or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_variable) + # Distribution variables + variables = self._get_distribution_variables(R) - # Volume-weighted particle-size distribution - f_v_dist = ( - R_variable * f_a_dist / - pybamm.Integral(R_variable * f_a_dist, R_variable) + # Standard distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables(c_s_distribution) ) # Standard R-averaged variables. Average concentrations using # the volume-weighted distribution since they are volume-based # quantities. Necessary for output variables "Total lithium in # negative electrode [mol]", etc, to be calculated correctly + f_v_dist = variables[ + self.domain + " volume-weighted particle-size distribution" + ] c_s = pybamm.Integral(f_v_dist * c_s_distribution, R_variable) c_s_xav = pybamm.x_average(c_s) - variables = self._get_standard_concentration_variables(c_s, c_s_xav) + variables.update(self._get_standard_concentration_variables(c_s, c_s_xav)) - # Standard distribution variables (R-dependent) - variables.update( - self._get_standard_concentration_distribution_variables(c_s_distribution) - ) - variables.update( - { - self.domain + " particle size": R_variable, - self.domain + " particle size [m]": R_variable * R_dim, - self.domain - + " area-weighted particle-size" - + " distribution": pybamm.x_average(f_a_dist), - self.domain - + " area-weighted particle-size" - + " distribution [m-1]": pybamm.x_average(f_a_dist) / R_dim, - self.domain + " volume-weighted particle-size" - + " distribution": pybamm.x_average(f_v_dist), - self.domain + " volume-weighted particle-size" - + " distribution [m-1]": pybamm.x_average(f_v_dist) / R_dim, - } - ) return variables def get_coupled_variables(self, variables): c_s_distribution = variables[ self.domain + " particle concentration distribution" ] - R_variable = variables[self.domain + " particle size"] - R = pybamm.PrimaryBroadcast(R_variable, [self.domain.lower() + " particle"],) + R_spatial_variable = variables[self.domain + " particle sizes"] + R = pybamm.PrimaryBroadcast( + R_spatial_variable, [self.domain.lower() + " particle"] + ) T_k = variables[self.domain + " electrode temperature"] # Variables can currently only have 3 domains, so remove "current collector" @@ -162,25 +114,19 @@ def get_coupled_variables(self, variables): * pybamm.grad(c_s_distribution) / R ) - f_a_dist = self.param.f_a_dist_n(R_variable) + f_a_dist = self.param.f_a_dist_n(R_spatial_variable) - # spatial var to use in R integral below (cannot use R_variable as - # it is a broadcast) - #R = pybamm.standard_spatial_vars.R_n elif self.domain == "Positive": N_s_distribution = ( -self.param.D_p(c_s_distribution, T_k) * pybamm.grad(c_s_distribution) / R ) - f_a_dist = self.param.f_a_dist_p(R_variable) - - # spatial var to use in R integral below (cannot use R_variable as - # it is a broadcast) - #R = pybamm.standard_spatial_vars.R_p + f_a_dist = self.param.f_a_dist_p(R_spatial_variable) # Standard R-averaged flux variables - N_s = pybamm.Integral(f_a_dist * N_s_distribution, R_variable) + # Use R_spatial_variable, since "R" is a broadcast + N_s = pybamm.Integral(f_a_dist * N_s_distribution, R_spatial_variable) variables.update(self._get_standard_flux_variables(N_s, N_s)) # Standard distribution flux variables (R-dependent) @@ -189,6 +135,7 @@ def get_coupled_variables(self, variables): ) variables.update(self._get_total_concentration_variables(variables)) + variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): @@ -198,8 +145,10 @@ def set_rhs(self, variables): N_s_distribution = variables[self.domain + " particle flux distribution"] - R_variable = variables[self.domain + " particle size"] - R = pybamm.PrimaryBroadcast(R_variable, [self.domain.lower() + " particle"],) + R_spatial_variable = variables[self.domain + " particle sizes"] + R = pybamm.PrimaryBroadcast( + R_spatial_variable, [self.domain.lower() + " particle"] + ) if self.domain == "Negative": self.rhs = { diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index 2b14f7ac48..c904818340 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -39,18 +39,14 @@ def get_fundamental_variables(self): bounds=(0, 1), ) # Since concentration does not depend on "x", need a particle-size - # spatial variable R with only "current collector" as secondary + # spatial variable R with only "current collector" as an auxiliary # domain - R_spatial_variable = pybamm.SpatialVariable( + R = pybamm.SpatialVariable( "R_n", domain=["negative particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) - R_dim = self.param.R_n_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_n(R_spatial_variable) elif self.domain == "Positive": # distribution variables @@ -64,56 +60,37 @@ def get_fundamental_variables(self): bounds=(0, 1), ) # Since concentration does not depend on "x", need a particle-size - # spatial variable R with only "current collector" as secondary + # spatial variable R with only "current collector" as an auxiliary # domain - R_spatial_variable = pybamm.SpatialVariable( + R = pybamm.SpatialVariable( "R_p", domain=["positive particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) - R_dim = self.param.R_p_typ - - # Particle-size distribution (area-weighted) - f_a_dist = self.param.f_a_dist_p(R_spatial_variable) - # Ensure the distribution is normalised, irrespective of discretisation - # or user input - f_a_dist = f_a_dist / pybamm.Integral(f_a_dist, R_spatial_variable) + # Distribution variables + variables = self._get_distribution_variables(R) - # Volume-weighted particle-size distribution - f_v_dist = ( - R_spatial_variable * f_a_dist / - pybamm.Integral(R_spatial_variable * f_a_dist, R_spatial_variable) + # Concentration distribution variables (R-dependent) + variables.update( + self._get_standard_concentration_distribution_variables( + c_s_xav_distribution + ) ) # Standard R-averaged variables. Average concentrations using # the volume-weighted distribution since they are volume-based # quantities. Necessary for output variables "Total lithium in # negative electrode [mol]", etc, to be calculated correctly - c_s_xav = pybamm.Integral(f_v_dist * c_s_xav_distribution, R_spatial_variable) + f_v_dist = variables[ + "X-averaged " + self.domain.lower() + + " volume-weighted particle-size distribution" + ] + c_s_xav = pybamm.Integral(f_v_dist * c_s_xav_distribution, R) c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) - variables = self._get_standard_concentration_variables(c_s, c_s_xav) - - # Standard concentration distribution variables (R-dependent) variables.update( - self._get_standard_concentration_distribution_variables( - c_s_xav_distribution - ) - ) - variables.update( - { - self.domain + " particle size": R_spatial_variable, - self.domain + " particle size [m]": R_spatial_variable * R_dim, - self.domain + " area-weighted particle-size" - + " distribution": f_a_dist, - self.domain + " area-weighted particle-size" - + " distribution [m-1]": f_a_dist / R_dim, - self.domain + " volume-weighted particle-size" - + " distribution": f_v_dist, - self.domain + " volume-weighted particle-size" - + " distribution [m-1]": f_v_dist / R_dim, - } + self._get_standard_concentration_variables(c_s, c_s_xav) ) return variables @@ -121,7 +98,7 @@ def get_coupled_variables(self, variables): c_s_xav_distribution = variables[ "X-averaged " + self.domain.lower() + " particle concentration distribution" ] - R_spatial_variable = variables[self.domain + " particle size"] + R_spatial_variable = variables[self.domain + " particle sizes"] # broadcast to "particle size" domain then again into "particle" T_k_xav = pybamm.PrimaryBroadcast( @@ -144,10 +121,15 @@ def get_coupled_variables(self, variables): # Standard R-averaged flux variables. Average using the area-weighted # distribution - f_a_dist = variables[self.domain + " area-weighted particle-size distribution"] + f_a_dist = variables[ + "X-averaged " + self.domain.lower() + + " area-weighted particle-size distribution" + ] f_a_dist = pybamm.PrimaryBroadcast( f_a_dist, [self.domain.lower() + " particle"], ) + # must use "R_spatial_variable" as integration variable, since "R" is a + # broadcast N_s_xav = pybamm.Integral(f_a_dist * N_s_xav_distribution, R_spatial_variable) N_s = pybamm.SecondaryBroadcast(N_s_xav, [self.domain.lower() + " electrode"]) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) @@ -161,7 +143,9 @@ def get_coupled_variables(self, variables): + " particle flux distribution": N_s_xav_distribution, } ) + variables.update(self._get_total_concentration_variables(variables)) + variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): @@ -175,7 +159,7 @@ def set_rhs(self, variables): ] # Spatial variable R, broadcast into particle - R_spatial_variable = variables[self.domain + " particle size"] + R_spatial_variable = variables[self.domain + " particle sizes"] R = pybamm.PrimaryBroadcast( R_spatial_variable, [self.domain.lower() + " particle"], ) @@ -207,7 +191,7 @@ def set_boundary_conditions(self, variables): + self.domain.lower() + " electrode interfacial current density distribution" ] - R = variables[self.domain + " particle size"] + R = variables[self.domain + " particle sizes"] # Extract x-av T and broadcast to particle size domain T_k_xav = variables[ diff --git a/tests/integration/test_models/standard_output_tests.py b/tests/integration/test_models/standard_output_tests.py index 69f95af4c0..fde4f57f26 100644 --- a/tests/integration/test_models/standard_output_tests.py +++ b/tests/integration/test_models/standard_output_tests.py @@ -82,12 +82,15 @@ def __init__(self, model, param, disc, solution, operating_condition): self.x_edge = disc.mesh.combine_submeshes(*whole_cell).edges * L_x if isinstance(self.model, pybamm.lithium_ion.BaseModel): - R_n = param.evaluate(model.param.R_n_typ) - R_p = param.evaluate(model.param.R_p_typ) - self.r_n = disc.mesh["negative particle"].nodes * R_n - self.r_p = disc.mesh["positive particle"].nodes * R_p - self.r_n_edge = disc.mesh["negative particle"].edges * R_n - self.r_p_edge = disc.mesh["positive particle"].edges * R_p + R_n_typ = param.evaluate(model.param.R_n_typ) + R_p_typ = param.evaluate(model.param.R_p_typ) + self.r_n = disc.mesh["negative particle"].nodes * R_n_typ + self.r_p = disc.mesh["positive particle"].nodes * R_p_typ + self.r_n_edge = disc.mesh["negative particle"].edges * R_n_typ + self.r_p_edge = disc.mesh["positive particle"].edges * R_p_typ + if self.model.options["particle size"] == "distribution": + self.R_n = disc.mesh["negative particle size"].nodes * R_n_typ + self.R_p = disc.mesh["positive particle size"].nodes * R_p_typ # Useful parameters self.l_n = param.evaluate(geo.l_n) @@ -276,6 +279,25 @@ def __init__(self, model, param, disc, solution, operating_condition): "Loss of lithium to positive electrode lithium plating [mol]" ] + if model.options["particle size"] == "distribution": + # These concentration variables are only present for distribution models. + + # Take only the x-averaged of these for now, since variables cannot have + # 4 domains yet + self.c_s_n_dist = solution[ + "X-averaged negative particle concentration distribution" + ] + self.c_s_p_dist = solution[ + "X-averaged positive particle concentration distribution" + ] + + self.c_s_n_surf_dist = solution[ + "Negative particle surface concentration distribution" + ] + self.c_s_p_surf_dist = solution[ + "Positive particle surface concentration distribution" + ] + def test_concentration_increase_decrease(self): """Test all concentrations in negative particles decrease and all concentrations in positive particles increase over a discharge.""" @@ -290,6 +312,25 @@ def test_concentration_increase_decrease(self): pos_diff = self.c_s_p_rav(t[1:], x_p) - self.c_s_p_rav(t[:-1], x_p) neg_end_vs_start = self.c_s_n_rav(t[-1], x_n) - self.c_s_n_rav(t[0], x_n) pos_end_vs_start = self.c_s_p_rav(t[-1], x_p) - self.c_s_p_rav(t[0], x_p) + elif self.model.options["particle size"] == "distribution": + R_n, R_p = self.R_n, self.R_p + # Test the concentration variables that depend on particle size + neg_diff = ( + self.c_s_n_dist(t[1:], r=r_n, R=R_n) - + self.c_s_n_dist(t[:-1], r=r_n, R=R_n) + ) + pos_diff = ( + self.c_s_p_dist(t[1:], r=r_p, R=R_p) - + self.c_s_p_dist(t[:-1], r=r_p, R=R_p) + ) + neg_end_vs_start = ( + self.c_s_n_dist(t[-1], r=r_n, R=R_n) - + self.c_s_n_dist(t[0], r=r_n, R=R_n) + ) + pos_end_vs_start = ( + self.c_s_p_dist(t[-1], r=r_p, R=R_p) - + self.c_s_p_dist(t[0], r=r_p, R=R_p) + ) else: neg_diff = self.c_s_n(t[1:], x_n, r_n) - self.c_s_n(t[:-1], x_n, r_n) pos_diff = self.c_s_p(t[1:], x_p, r_p) - self.c_s_p(t[:-1], x_p, r_p) @@ -321,6 +362,13 @@ def test_concentration_limits(self): np.testing.assert_array_less(self.c_s_n(t, x_n, r_n), 1) np.testing.assert_array_less(self.c_s_p(t, x_p, r_p), 1) + if self.model.options["particle size"] == "distribution": + R_n, R_p = self.R_n, self.R_p + np.testing.assert_array_less(-self.c_s_n_dist(t, r=r_n, R=R_n), 0) + np.testing.assert_array_less(-self.c_s_p_dist(t, r=r_p, R=R_p), 0) + + np.testing.assert_array_less(self.c_s_n_dist(t, r=r_n, R=R_n), 1) + np.testing.assert_array_less(self.c_s_p_dist(t, r=r_p, R=R_p), 1) def test_conservation(self): """Test amount of lithium stored across all particles and in SEI layers is diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py index 05a59f0d47..56ae1aaef2 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py @@ -159,6 +159,88 @@ def test_compare_particle_shape(self): comparison = StandardOutputComparison(solutions) comparison.test_all(skip_first_timestep=True) + def test_compare_narrow_size_distribution(self): + # The MPM should agree with the SPM when the size distributions are narrow + # enough. + models = [ + pybamm.lithium_ion.SPM(), pybamm.lithium_ion.MPM() + ] + + param = models[0].default_parameter_values + + # Set size distribution parameters + R_n_dim = param["Negative particle radius [m]"] + R_p_dim = param["Positive particle radius [m]"] + + # Very small standard deviations + sd_a_n = 0.05 + sd_a_p = 0.05 + + # Min and max radii + R_min_n = 0.8 + R_min_p = 0.8 + R_max_n = 1.2 + R_max_p = 1.2 + + def lognormal(R, R_av, sd): + import numpy as np + + mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) + return ( + pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / R + ) + + def f_a_dist_n_dim(R): + return lognormal(R, R_n_dim, sd_a_n * R_n_dim) + + def f_a_dist_p_dim(R): + return lognormal(R, R_p_dim, sd_a_p * R_p_dim) + + param.update( + { + "Negative minimum particle radius [m]": R_min_n * R_n_dim, + "Positive minimum particle radius [m]": R_min_p * R_p_dim, + "Negative maximum particle radius [m]": R_max_n * R_n_dim, + "Positive maximum particle radius [m]": R_max_p * R_p_dim, + "Negative area-weighted " + + "particle-size distribution [m-1]": f_a_dist_n_dim, + "Positive area-weighted " + + "particle-size distribution [m-1]": f_a_dist_p_dim, + }, + check_already_exists=False, + ) + + # set same mesh for both models + 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.R_n: 5, + var.R_p: 5, + } + + # solve models + solutions = [] + for model in models: + sim = pybamm.Simulation( + model, + var_pts=var_pts, + parameter_values=param, + solver=pybamm.CasadiSolver(mode="fast") + ) + solution = sim.solve([0, 3600]) + solutions.append(solution) + + # compare outputs + comparison = StandardOutputComparison(solutions) + comparison.test_all(skip_first_timestep=True) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index a3aceff558..df56515f9d 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -13,8 +13,8 @@ def test_basic_processing(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) # use Ecker parameters for nonlinear diffusion - #param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) - #param = self.add_distribution_params_for_test(param) + param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) + param = self.set_distribution_params_for_test(param) modeltest = tests.StandardModelTest(model) modeltest.test_all() @@ -96,118 +96,38 @@ def test_particle_uniform(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - def test_loss_active_material_stress_negative(self): - options = {"loss of active material": ("none", "stress-driven")} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def test_loss_active_material_stress_positive(self): - options = {"loss of active material": ("stress-driven", "none")} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def test_loss_active_material_stress_both(self): - options = {"loss of active material": "stress-driven"} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def add_distribution_params_for_test(self, param): - R_n_dim = param["Negative particle radius [m]"] - R_p_dim = param["Positive particle radius [m]"] - sd_a_n = 0.3 - sd_a_p = 0.3 - - # Min and max radii - R_min_n = 0 - R_min_p = 0 - R_max_n = 1 + sd_a_n * 5 - R_max_p = 1 + sd_a_p * 5 - - def lognormal_distribution(R, R_av, sd): - import numpy as np - - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / (R) - ) - - # Set the dimensional (area-weighted) particle-size distributions - def f_a_dist_n_dim(R): - return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) - - def f_a_dist_p_dim(R): - return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) - - # Append to parameter set - param.update( - { - "Negative minimum particle radius [m]": R_min_n * R_n_dim, - "Positive minimum particle radius [m]": R_min_p * R_p_dim, - "Negative maximum particle radius [m]": R_max_n * R_n_dim, - "Positive maximum particle radius [m]": R_max_p * R_p_dim, - "Negative area-weighted " - + "particle-size distribution [m-1]": f_a_dist_n_dim, - "Positive area-weighted " - + "particle-size distribution [m-1]": f_a_dist_p_dim, - }, - check_already_exists=False, - ) - return param - - -class TestMPMWithCrack(unittest.TestCase): - def test_well_posed_negative_cracking(self): - options = {"particle mechanics": ("swelling and cracking", "none")} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def test_well_posed_positive_cracking(self): - options = {"particle mechanics": ("none", "swelling and cracking")} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def test_well_posed_both_cracking(self): - options = {"particle mechanics": "swelling and cracking"} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def test_well_posed_both_swelling_only(self): - options = {"particle mechanics": "swelling only"} - model = pybamm.lithium_ion.MPM(options) - chemistry = pybamm.parameter_sets.Ai2020 - parameter_values = pybamm.ParameterValues(chemistry=chemistry) - param = self.add_distribution_params_for_test(parameter_values) - modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() + def test_conservation_each_electrode(self): + # Test that surface areas are being calculated from the distribution correctly + # for any discretization in the size domain. + # We test that the amount of lithium removed or added to each electrode + # is the same as for the SPM with the same parameters + models = [pybamm.lithium_ion.SPM(), pybamm.lithium_ion.MPM()] + var = pybamm.standard_spatial_vars - def add_distribution_params_for_test(self, param): + # reduce number of particle sizes, for a crude discretization + var_pts = { + var.R_n: 3, + var.R_p: 3, + } + solver = pybamm.CasadiSolver(mode="fast") + + # solve + neg_Li = [] + pos_Li = [] + for model in models: + sim = pybamm.Simulation(model, solver=solver) + sim.var_pts.update(var_pts) + solution = sim.solve([0, 3500]) + neg = solution["Total lithium in negative electrode [mol]"].entries[-1] + pos = solution["Total lithium in positive electrode [mol]"].entries[-1] + neg_Li.append(neg) + pos_Li.append(pos) + + # compare + np.testing.assert_array_almost_equal(neg_Li[0], neg_Li[1], decimal=14) + np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=14) + + def set_distribution_params_for_test(self, param): R_n_dim = param["Negative particle radius [m]"] R_p_dim = param["Positive particle radius [m]"] sd_a_n = 0.3 @@ -219,7 +139,7 @@ def add_distribution_params_for_test(self, param): R_max_n = 1 + sd_a_n * 5 R_max_p = 1 + sd_a_p * 5 - def lognormal_distribution(R, R_av, sd): + def lognormal(R, R_av, sd): import numpy as np mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) @@ -232,10 +152,10 @@ def lognormal_distribution(R, R_av, sd): # Set the dimensional (area-weighted) particle-size distributions def f_a_dist_n_dim(R): - return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) + return lognormal(R, R_n_dim, sd_a_n * R_n_dim) def f_a_dist_p_dim(R): - return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) + return lognormal(R, R_p_dim, sd_a_p * R_p_dim) # Append to parameter set param.update( From 31855e71e97e9e76af898a07a2e9db7628498bff Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 11 Jun 2021 16:11:37 +0100 Subject: [PATCH 36/67] incompatible options with size distributions --- .../full_battery_models/base_battery_model.py | 27 ++++++++++ .../full_battery_models/lithium_ion/mpm.py | 14 ++--- .../test_lithium_ion/test_mpm.py | 54 ++++++++----------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 8c9ff66638..9815f2168c 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -277,6 +277,33 @@ def __init__(self, extra_options): "current density as a state' must be 'true'" ) + # Options not yet compatible with particle-size distributions + if options["particle size"] == "distribution": + if options["SEI"] != "none": + raise NotImplementedError( + "SEI submodels do not yet support particle-size distributions." + ) + if options["lithium plating"] != "none": + raise NotImplementedError( + "Lithium plating submodels do not yet support particle-size " + "distributions." + ) + if options["particle mechanics"] != "none": + raise NotImplementedError( + "Particle mechanics submodels do not yet support particle-size" + " distributions." + ) + if options["particle shape"] != "spherical": + raise NotImplementedError( + "Particle shape must be 'spherical' for particle-size distributions" + " submodels." + ) + if options["thermal"] == "x-full": + raise NotImplementedError( + "X-full thermal submodels do not yet support particle-size" + " distributions." + ) + # Some standard checks to make sure options are compatible if options["SEI porosity change"] in [True, False]: raise pybamm.OptionError( diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 9a60ef86f4..f5bff6cb27 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -117,6 +117,13 @@ def set_interfacial_submodel(self): def set_particle_submodel(self): + if self.options["particle size"] != "distribution": + raise pybamm.OptionError( + "particle size must be 'distribution' for MPM not '{}'".format( + self.options["particle size"] + ) + ) + if self.options["particle"] == "Fickian diffusion": submod_n = pybamm.particle.FickianSingleSizeDistribution( self.param, "Negative" @@ -208,12 +215,7 @@ def set_thermal_submodel(self): def set_sei_submodel(self): # negative electrode SEI - if self.options["SEI"] == "none": - self.submodels["negative sei"] = pybamm.sei.NoSEI(self.param, "Negative") - else: - raise NotImplementedError( - """SEI submodels do not yet support particle-size distributions.""" - ) + self.submodels["negative sei"] = pybamm.sei.NoSEI(self.param, "Negative") # positive electrode self.submodels["positive sei"] = pybamm.sei.NoSEI(self.param, "Positive") diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 2147a59dde..b7c6c788d2 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -96,28 +96,23 @@ def test_particle_uniform(self): def test_particle_shape_user(self): options = {"particle shape": "user"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_loss_active_material_stress_negative(self): options = {"loss of active material": ("stress-driven", "none")} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_loss_active_material_stress_positive(self): options = {"loss of active material": ("none", "stress-driven")} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_loss_active_material_stress_both(self): options = {"loss of active material": "stress-driven"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - - def test_loss_active_material_stress_reaction_both(self): - options = {"loss of active material": "reaction-driven"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_electrolyte_options(self): options = {"electrolyte conductivity": "full"} @@ -155,8 +150,8 @@ def test_well_posed_reversible_plating_with_porosity(self): "lithium plating": "reversible", "lithium plating porosity change": "true", } - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) class TestMPMExternalCircuits(unittest.TestCase): @@ -211,40 +206,35 @@ def test_ec_reaction_limited_not_implemented(self): class TestMPMWithCrack(unittest.TestCase): def test_well_posed_negative_cracking(self): options = {"particle mechanics": ("swelling and cracking", "none")} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_well_posed_positive_cracking(self): options = {"particle mechanics": ("none", "swelling and cracking")} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_well_posed_both_cracking(self): options = {"particle mechanics": "swelling and cracking"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_well_posed_both_swelling_only(self): options = {"particle mechanics": "swelling only"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) class TestMPMWithPlating(unittest.TestCase): - def test_well_posed_none_plating(self): - options = {"lithium plating": "none"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - def test_well_posed_reversible_plating(self): options = {"lithium plating": "reversible"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) def test_well_posed_irreversible_plating(self): options = {"lithium plating": "irreversible"} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) if __name__ == "__main__": From 23b9f4a2d8bfac9151ce2045e65cd4bee9dfc925 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 11 Jun 2021 18:13:11 +0100 Subject: [PATCH 37/67] rename mean radii variables --- .../size_distribution/base_distribution.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index 9967c5adb8..d05d1dbbad 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -94,17 +94,17 @@ def _get_distribution_variables(self, R): + " distribution": f_num_dist, self.domain + " number-based particle-size" + " distribution [m-1]": f_num_dist / R_typ, - "True " + self.domain.lower() + " area-weighted" + self.domain + " area-weighted" + " mean radius": true_R_a_mean, - "True " + self.domain.lower() + " area-weighted" + self.domain + " area-weighted" + " mean radius [m]": true_R_a_mean * R_typ, - "True " + self.domain.lower() + " volume-weighted" + self.domain + " volume-weighted" + " mean radius": true_R_v_mean, - "True " + self.domain.lower() + " volume-weighted" + self.domain + " volume-weighted" + " mean radius [m]": true_R_v_mean * R_typ, - "True " + self.domain.lower() + " number-based" + self.domain + " number-based" + " mean radius": true_R_num_mean, - "True " + self.domain.lower() + " number-based" + self.domain + " number-based" + " mean radius [m]": true_R_num_mean * R_typ, # X-averaged distributions "X-averaged " + self.domain.lower() + @@ -249,13 +249,13 @@ def _get_surface_area_output_variables(self, variables): elif self.domain == "Positive": a_typ = self.param.a_p_typ - true_R_a_mean = variables[ - "True " + self.domain.lower() + " area-weighted mean radius" - ] + R_a_mean = variables[self.domain + " area-weighted mean radius"] # True surface area to volume ratio, using the true area-weighted mean - # radius calculated from the distribution - true_a = 1 / true_R_a_mean + # radius calculated from the distribution. It should agree with the + # "X-averaged negative electrode surface area to volume ratio", etc., + # calculated in `active_material.BaseModel`, and can be used as a check + true_a = 1 / R_a_mean variables.update( { From ea1c372d890b6524f8ad23ede04c6042131f1dd2 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 15 Jun 2021 12:36:14 +0100 Subject: [PATCH 38/67] added voltage control to MPM --- .../full_battery_models/lithium_ion/mpm.py | 24 ------------------- .../test_lithium_ion/test_mpm.py | 12 ++++++++-- .../test_lithium_ion/test_mpm.py | 6 ++--- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index f5bff6cb27..e97469a991 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -72,30 +72,6 @@ def __init__( pybamm.citations.register("Kirk2020") - def set_external_circuit_submodel(self): - """ - Define how the external circuit defines the boundary conditions for the model, - e.g. (not necessarily constant-) current, voltage, etc - """ - if self.options["operating mode"] == "current": - self.submodels["external circuit"] = pybamm.external_circuit.CurrentControl( - self.param - ) - elif self.options["operating mode"] == "voltage": - raise NotImplementedError( - """Many-Particle Model does not support voltage control.""" - ) - elif self.options["operating mode"] == "power": - self.submodels[ - "external circuit" - ] = pybamm.external_circuit.PowerFunctionControl(self.param) - elif callable(self.options["operating mode"]): - self.submodels[ - "external circuit" - ] = pybamm.external_circuit.FunctionControl( - self.param, self.options["operating mode"] - ) - def set_convection_submodel(self): self.submodels[ diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index df56515f9d..b17911cdf6 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -96,6 +96,14 @@ def test_particle_uniform(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() + def test_voltage_control(self): + options = {"operating mode": "voltage"} + model = pybamm.lithium_ion.MPM(options) + param = model.default_parameter_values + param.update({"Voltage function [V]": 3.8}, check_already_exists=False) + modeltest = tests.StandardModelTest(model, parameter_values=param) + modeltest.test_all(skip_output_tests=True) + def test_conservation_each_electrode(self): # Test that surface areas are being calculated from the distribution correctly # for any discretization in the size domain. @@ -128,6 +136,8 @@ def test_conservation_each_electrode(self): np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=14) def set_distribution_params_for_test(self, param): + import numpy as np + R_n_dim = param["Negative particle radius [m]"] R_p_dim = param["Positive particle radius [m]"] sd_a_n = 0.3 @@ -140,8 +150,6 @@ def set_distribution_params_for_test(self, param): R_max_p = 1 + sd_a_p * 5 def lognormal(R, R_av, sd): - import numpy as np - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) return ( diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index b7c6c788d2..7d983650bc 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -155,10 +155,10 @@ def test_well_posed_reversible_plating_with_porosity(self): class TestMPMExternalCircuits(unittest.TestCase): - def test_voltage_not_implemented(self): + def test_well_posed_voltage(self): options = {"operating mode": "voltage"} - with self.assertRaises(NotImplementedError): - pybamm.lithium_ion.MPM(options) + model = pybamm.lithium_ion.MPM(options) + model.check_well_posedness() def test_well_posed_power(self): options = {"operating mode": "power"} From 57b706a406cb63865b076f9d65aacdb37fcb6b69 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 15 Jun 2021 14:17:00 +0100 Subject: [PATCH 39/67] added more distribution output vars --- .../size_distribution/base_distribution.py | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index d05d1dbbad..dc236264dd 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -48,17 +48,25 @@ def _get_distribution_variables(self, R): # Number-based particle-size distribution f_num_dist = (f_a_dist / R ** 2) / pybamm.Integral(f_a_dist / R ** 2, R) - # True mean radii, given the f_a_dist that was given - true_R_num_mean = pybamm.Integral(R * f_num_dist, R) - true_R_a_mean = pybamm.Integral(R * f_a_dist, R) - true_R_v_mean = pybamm.Integral(R * f_v_dist, R) - - # X-average the mean radii (to remove the "electrode" domain, if present) - true_R_num_mean = pybamm.x_average(true_R_num_mean) - true_R_a_mean = pybamm.x_average(true_R_a_mean) - true_R_v_mean = pybamm.x_average(true_R_v_mean) - - # X-averaged distributions + # True mean radii and standard deviations, calculated from the f_a_dist that + # was given + R_num_mean = pybamm.Integral(R * f_num_dist, R) + R_a_mean = pybamm.Integral(R * f_a_dist, R) + R_v_mean = pybamm.Integral(R * f_v_dist, R) + sd_num = pybamm.sqrt(pybamm.Integral((R - R_num_mean) ** 2 * f_num_dist, R)) + sd_a = pybamm.sqrt(pybamm.Integral((R - R_a_mean) ** 2 * f_a_dist, R)) + sd_v = pybamm.sqrt(pybamm.Integral((R - R_v_mean) ** 2 * f_v_dist, R)) + + # X-average the means and standard deviations to give scalars + # (to remove the "electrode" domain, if present) + R_num_mean = pybamm.x_average(R_num_mean) + R_a_mean = pybamm.x_average(R_a_mean) + R_v_mean = pybamm.x_average(R_v_mean) + sd_num = pybamm.x_average(sd_num) + sd_a = pybamm.x_average(sd_a) + sd_v = pybamm.x_average(sd_v) + + # X-averaged distributions, or broadcast if R.auxiliary_domains["secondary"] == [self.domain.lower() + " electrode"]: f_a_dist_xav = pybamm.x_average(f_a_dist) f_v_dist_xav = pybamm.x_average(f_v_dist) @@ -95,17 +103,29 @@ def _get_distribution_variables(self, R): self.domain + " number-based particle-size" + " distribution [m-1]": f_num_dist / R_typ, self.domain + " area-weighted" - + " mean radius": true_R_a_mean, + + " mean radius": R_a_mean, + self.domain + " area-weighted" + + " mean radius [m]": R_a_mean * R_typ, + self.domain + " volume-weighted" + + " mean radius": R_v_mean, + self.domain + " volume-weighted" + + " mean radius [m]": R_v_mean * R_typ, + self.domain + " number-based" + + " mean radius": R_num_mean, + self.domain + " number-based" + + " mean radius [m]": R_num_mean * R_typ, + self.domain + " area-weighted" + + " standard deviation": sd_a, self.domain + " area-weighted" - + " mean radius [m]": true_R_a_mean * R_typ, + + " standard deviation [m]": sd_a * R_typ, self.domain + " volume-weighted" - + " mean radius": true_R_v_mean, + + " standard deviation": sd_v, self.domain + " volume-weighted" - + " mean radius [m]": true_R_v_mean * R_typ, + + " standard deviation [m]": sd_v * R_typ, self.domain + " number-based" - + " mean radius": true_R_num_mean, + + " standard deviation": sd_num, self.domain + " number-based" - + " mean radius [m]": true_R_num_mean * R_typ, + + " standard deviation [m]": sd_num * R_typ, # X-averaged distributions "X-averaged " + self.domain.lower() + " area-weighted particle-size distribution": f_a_dist_xav, From 8f4b30e4f9e460a28b20afd4f0c2d0ac5bed199f Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 15 Jun 2021 14:53:58 +0100 Subject: [PATCH 40/67] added doc index files for MPM and submodels --- docs/source/expression_tree/unary_operator.rst | 2 ++ docs/source/models/lithium_ion/index.rst | 1 + docs/source/models/lithium_ion/mpm.rst | 5 +++++ docs/source/models/submodels/particle/index.rst | 1 + .../particle/size_distribution/base_distribution.rst | 6 ++++++ .../size_distribution/fast_many_distributions.rst | 5 +++++ .../size_distribution/fast_single_distribution.rst | 5 +++++ .../size_distribution/fickian_many_distributions.rst | 7 +++++++ .../size_distribution/fickian_single_distribution.rst | 6 ++++++ .../submodels/particle/size_distribution/index.rst | 11 +++++++++++ pybamm/expression_tree/unary_operators.py | 5 +++-- .../particle/size_distribution/base_distribution.py | 2 +- .../size_distribution/fast_many_distributions.py | 2 +- .../size_distribution/fast_single_distribution.py | 2 +- .../size_distribution/fickian_many_distributions.py | 2 +- .../size_distribution/fickian_single_distribution.py | 2 +- 16 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 docs/source/models/lithium_ion/mpm.rst create mode 100644 docs/source/models/submodels/particle/size_distribution/base_distribution.rst create mode 100644 docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst create mode 100644 docs/source/models/submodels/particle/size_distribution/fast_single_distribution.rst create mode 100644 docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst create mode 100644 docs/source/models/submodels/particle/size_distribution/fickian_single_distribution.rst create mode 100644 docs/source/models/submodels/particle/size_distribution/index.rst diff --git a/docs/source/expression_tree/unary_operator.rst b/docs/source/expression_tree/unary_operator.rst index 88166ded3c..1a96e5d29a 100644 --- a/docs/source/expression_tree/unary_operator.rst +++ b/docs/source/expression_tree/unary_operator.rst @@ -81,6 +81,8 @@ Unary Operators .. autofunction:: pybamm.r_average +.. autofunction:: pybamm.R_average + .. autofunction:: pybamm.z_average .. autofunction:: pybamm.yz_average diff --git a/docs/source/models/lithium_ion/index.rst b/docs/source/models/lithium_ion/index.rst index 07cbed2c7e..040a0dad39 100644 --- a/docs/source/models/lithium_ion/index.rst +++ b/docs/source/models/lithium_ion/index.rst @@ -6,6 +6,7 @@ Lithium-ion Models base_lithium_ion_model spm spme + mpm dfn newman_tobias yang2017 diff --git a/docs/source/models/lithium_ion/mpm.rst b/docs/source/models/lithium_ion/mpm.rst new file mode 100644 index 0000000000..bd9fd03b50 --- /dev/null +++ b/docs/source/models/lithium_ion/mpm.rst @@ -0,0 +1,5 @@ +Many Particle Model (MPM) +=========================== + +.. autoclass:: pybamm.lithium_ion.MPM + :members: diff --git a/docs/source/models/submodels/particle/index.rst b/docs/source/models/submodels/particle/index.rst index 71c2ac483a..ad9676f4dd 100644 --- a/docs/source/models/submodels/particle/index.rst +++ b/docs/source/models/submodels/particle/index.rst @@ -9,3 +9,4 @@ Particle fickian_many_particles polynomial_single_particle polynomial_many_particles + size_distribution/index diff --git a/docs/source/models/submodels/particle/size_distribution/base_distribution.rst b/docs/source/models/submodels/particle/size_distribution/base_distribution.rst new file mode 100644 index 0000000000..43182a4531 --- /dev/null +++ b/docs/source/models/submodels/particle/size_distribution/base_distribution.rst @@ -0,0 +1,6 @@ +Particle Size Distribution Base Model +===================================== + +.. autoclass:: pybamm.particle.BaseSizeDistribution + :members: + diff --git a/docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst b/docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst new file mode 100644 index 0000000000..97f0feed64 --- /dev/null +++ b/docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst @@ -0,0 +1,5 @@ +Fast Many Size Distributions +============================ + +.. autoclass:: pybamm.particle.FastManySizeDistributions + :members: diff --git a/docs/source/models/submodels/particle/size_distribution/fast_single_distribution.rst b/docs/source/models/submodels/particle/size_distribution/fast_single_distribution.rst new file mode 100644 index 0000000000..22d281f56c --- /dev/null +++ b/docs/source/models/submodels/particle/size_distribution/fast_single_distribution.rst @@ -0,0 +1,5 @@ +Fast Single Size Distribution +============================= + +.. autoclass:: pybamm.particle.FastSingleSizeDistribution + :members: diff --git a/docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst b/docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst new file mode 100644 index 0000000000..7b927b52cc --- /dev/null +++ b/docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst @@ -0,0 +1,7 @@ +Fickian Many Size Distributions +=============================== + +.. autoclass:: pybamm.particle.FickianManySizeDistributions + :members: + + diff --git a/docs/source/models/submodels/particle/size_distribution/fickian_single_distribution.rst b/docs/source/models/submodels/particle/size_distribution/fickian_single_distribution.rst new file mode 100644 index 0000000000..6ed09a1d7c --- /dev/null +++ b/docs/source/models/submodels/particle/size_distribution/fickian_single_distribution.rst @@ -0,0 +1,6 @@ +Fickian Single Size Distribution +================================ + +.. autoclass:: pybamm.particle.FickianSingleSizeDistribution + :members: + diff --git a/docs/source/models/submodels/particle/size_distribution/index.rst b/docs/source/models/submodels/particle/size_distribution/index.rst new file mode 100644 index 0000000000..1efda8f7cd --- /dev/null +++ b/docs/source/models/submodels/particle/size_distribution/index.rst @@ -0,0 +1,11 @@ +Particle Size Distribution +========================== + +.. toctree:: + :maxdepth: 1 + + base_distribution + fickian_single_distribution + fickian_many_distributions + fast_single_distribution + fast_many_distributions diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index bc5b332997..c71176d28e 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1345,14 +1345,15 @@ def r_average(symbol): def R_average(symbol, param): - """convenience function for averaging over particle size R. + """convenience function for averaging over particle size R using the area-weighted + particle-size distribution. Parameters ---------- symbol : :class:`pybamm.Symbol` The function to be averaged param : :class:`pybamm.LithiumIonParameters` - The parameter object containing the particle-size distributions + The parameter object containing the area-weighted particle-size distributions f_a_dist_n and f_a_dist_p. Returns ------- diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index dc236264dd..d61cf36c42 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -17,7 +17,7 @@ class BaseSizeDistribution(BaseParticle): domain : str The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.BaseParticle` + **Extends:** :class:`pybamm.particle.BaseParticle` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index 7bdcc64071..b66f77b0e4 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -21,7 +21,7 @@ class FastManySizeDistributions(BaseSizeDistribution): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` + **Extends:** :class:`pybamm.particle.BaseSizeDistribution` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index 672e1e0362..5f4954af17 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -20,7 +20,7 @@ class FastSingleSizeDistribution(BaseSizeDistribution): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` + **Extends:** :class:`pybamm.particle.BaseSizeDistribution` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py index e3f8d74161..0fd736863c 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py @@ -20,7 +20,7 @@ class FickianManySizeDistributions(BaseSizeDistribution): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` + **Extends:** :class:`pybamm.particle.BaseSizeDistribution` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index c904818340..50f2de9fd5 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -19,7 +19,7 @@ class FickianSingleSizeDistribution(BaseSizeDistribution): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.size_distribution.BaseSizeDistribution` + **Extends:** :class:`pybamm.particle.BaseSizeDistribution` """ def __init__(self, param, domain): From a11b5ecfd4cfd0d4b060a9d3125cbfff984913bf Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 15 Jun 2021 15:02:46 +0100 Subject: [PATCH 41/67] fix distribution output variables --- .../size_distribution/base_distribution.py | 48 +++++-------------- .../fast_many_distributions.py | 1 - .../fast_single_distribution.py | 1 - .../fickian_many_distributions.py | 1 - .../fickian_single_distribution.py | 1 - 5 files changed, 12 insertions(+), 40 deletions(-) diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index d61cf36c42..379480d503 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -103,28 +103,28 @@ def _get_distribution_variables(self, R): self.domain + " number-based particle-size" + " distribution [m-1]": f_num_dist / R_typ, self.domain + " area-weighted" - + " mean radius": R_a_mean, + + " mean particle radius": R_a_mean, self.domain + " area-weighted" - + " mean radius [m]": R_a_mean * R_typ, + + " mean particle radius [m]": R_a_mean * R_typ, self.domain + " volume-weighted" - + " mean radius": R_v_mean, + + " mean particle radius": R_v_mean, self.domain + " volume-weighted" - + " mean radius [m]": R_v_mean * R_typ, + + " mean particle radius [m]": R_v_mean * R_typ, self.domain + " number-based" - + " mean radius": R_num_mean, + + " mean particle radius": R_num_mean, self.domain + " number-based" - + " mean radius [m]": R_num_mean * R_typ, - self.domain + " area-weighted" + + " mean particle radius [m]": R_num_mean * R_typ, + self.domain + " area-weighted particle-size" + " standard deviation": sd_a, - self.domain + " area-weighted" + self.domain + " area-weighted particle-size" + " standard deviation [m]": sd_a * R_typ, - self.domain + " volume-weighted" + self.domain + " volume-weighted particle-size" + " standard deviation": sd_v, - self.domain + " volume-weighted" + self.domain + " volume-weighted particle-size" + " standard deviation [m]": sd_v * R_typ, - self.domain + " number-based" + self.domain + " number-based particle-size" + " standard deviation": sd_num, - self.domain + " number-based" + self.domain + " number-based particle-size" + " standard deviation [m]": sd_num * R_typ, # X-averaged distributions "X-averaged " + self.domain.lower() + @@ -262,27 +262,3 @@ def _get_standard_concentration_distribution_variables(self, c_s): } return variables - def _get_surface_area_output_variables(self, variables): - - if self.domain == "Negative": - a_typ = self.param.a_n_typ - elif self.domain == "Positive": - a_typ = self.param.a_p_typ - - R_a_mean = variables[self.domain + " area-weighted mean radius"] - - # True surface area to volume ratio, using the true area-weighted mean - # radius calculated from the distribution. It should agree with the - # "X-averaged negative electrode surface area to volume ratio", etc., - # calculated in `active_material.BaseModel`, and can be used as a check - true_a = 1 / R_a_mean - - variables.update( - { - "True " + self.domain.lower() + " electrode surface area to volume" - + " ratio" : true_a, - "True " + self.domain.lower() + " electrode surface area to volume" - + " ratio [m-1]" : true_a * a_typ, - } - ) - return variables diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index b66f77b0e4..c704638ccc 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -99,7 +99,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): variables.update(self._get_total_concentration_variables(variables)) - variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index 5f4954af17..4750fd9228 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -111,7 +111,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): variables.update(self._get_total_concentration_variables(variables)) - variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py index 0fd736863c..a624a23c10 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py @@ -135,7 +135,6 @@ def get_coupled_variables(self, variables): ) variables.update(self._get_total_concentration_variables(variables)) - variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index 50f2de9fd5..94382c982c 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -145,7 +145,6 @@ def get_coupled_variables(self, variables): ) variables.update(self._get_total_concentration_variables(variables)) - variables.update(self._get_surface_area_output_variables(variables)) return variables def set_rhs(self, variables): From a6582b6383dc1982237c713ff6f76b70428abf4d Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 15:39:52 +0100 Subject: [PATCH 42/67] add output vars to fix lead acid tests --- .../submodels/active_material/base_active_material.py | 4 ++++ pybamm/solvers/processed_variable.py | 11 ++++++++--- .../test_leading_surface_form_conductivity.py | 2 ++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pybamm/models/submodels/active_material/base_active_material.py b/pybamm/models/submodels/active_material/base_active_material.py index f43aa330d8..610ab38c31 100644 --- a/pybamm/models/submodels/active_material/base_active_material.py +++ b/pybamm/models/submodels/active_material/base_active_material.py @@ -49,6 +49,10 @@ def _get_standard_active_material_variables(self, eps_solid): self.domain + " electrode surface area to volume ratio": a, self.domain + " electrode surface area to volume ratio [m-1]": a * a_typ, + "X-averaged " + self.domain.lower() + + " electrode surface area to volume ratio": pybamm.x_average(a), + "X-averaged " + self.domain.lower() + " electrode surface area" + + " to volume ratio [m-1]": pybamm.x_average(a) * a_typ, } ) return variables diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 5dd32784cf..479729432f 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -360,9 +360,14 @@ def initialise_2D(self): # assign attributes for reference self.entries = entries self.dimensions = 2 - first_length_scale = self.get_spatial_scale( - self.first_dimension, self.domain[0] - ) + if self.first_dimension == "r" and self.second_dimension == "R": + # for an r-R variable, must leave r nondimensional as it was scaled using + # R + first_length_scale = 1 + else: + first_length_scale = self.get_spatial_scale( + self.first_dimension, self.domain[0] + ) first_dim_pts_for_interp = first_dim_pts * first_length_scale second_length_scale = self.get_spatial_scale( diff --git a/tests/unit/test_models/test_submodels/test_electrolyte_conductivity/test_surface_form/test_leading_surface_form_conductivity.py b/tests/unit/test_models/test_submodels/test_electrolyte_conductivity/test_surface_form/test_leading_surface_form_conductivity.py index 7f2de31c53..db35cee79f 100644 --- a/tests/unit/test_models/test_submodels/test_electrolyte_conductivity/test_surface_form/test_leading_surface_form_conductivity.py +++ b/tests/unit/test_models/test_submodels/test_electrolyte_conductivity/test_surface_form/test_leading_surface_form_conductivity.py @@ -24,6 +24,7 @@ def test_public_functions(self): "Negative electrolyte concentration": a_n, "Sum of x-averaged negative electrode interfacial current densities": a, "X-averaged negative electrode total interfacial current density": a, + "X-averaged negative electrode surface area to volume ratio": a, } spf = pybamm.electrolyte_conductivity.surface_potential_form submodel = spf.LeadingOrderAlgebraic(param, "Negative") @@ -43,6 +44,7 @@ def test_public_functions(self): "Positive electrolyte concentration": a_p, "Sum of x-averaged positive electrode interfacial current densities": a, "X-averaged positive electrode total interfacial current density": a, + "X-averaged positive electrode surface area to volume ratio": a, } submodel = spf.LeadingOrderAlgebraic(param, "Positive") std_tests = tests.StandardSubModelTests(submodel, variables) From e2a0840bda2802534e1a0790466c5a628e62150c Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 15:47:15 +0100 Subject: [PATCH 43/67] added Kirk2021 citation --- pybamm/CITATIONS.txt | 14 ++++++++++++++ .../models/full_battery_models/lithium_ion/mpm.py | 1 + .../size_distribution/fast_many_distributions.py | 2 +- .../fickian_many_distributions.py | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pybamm/CITATIONS.txt b/pybamm/CITATIONS.txt index 4eb7287da9..f567da06a5 100644 --- a/pybamm/CITATIONS.txt +++ b/pybamm/CITATIONS.txt @@ -414,4 +414,18 @@ doi={10.1149/2.0661810jes} url = {https://arxiv.org/abs/2006.12208}, archiveprefix = {arXiv}, primaryclass = {physics.app-ph}, +} + +@article{Kirk2021, + author = {Toby L. Kirk and Colin P. Please and S. Jon Chapman}, + title = {Physical Modelling of the Slow Voltage Relaxation Phenomenon in Lithium-Ion Batteries}, + journal = {Journal of The Electrochemical Society}, + year = 2021, + month = {jun}, + publisher = {The Electrochemical Society}, + volume = {168}, + number = {6}, + pages = {060554}, + doi = {10.1149/1945-7111/ac0bf7}, + url = {https://doi.org/10.1149/1945-7111/ac0bf7}, } \ No newline at end of file diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index e97469a991..fa7dce9a4e 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -71,6 +71,7 @@ def __init__( self.build_model() pybamm.citations.register("Kirk2020") + pybamm.citations.register("Kirk2021") def set_convection_submodel(self): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py index c704638ccc..6815af81fd 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py @@ -26,7 +26,7 @@ class FastManySizeDistributions(BaseSizeDistribution): def __init__(self, param, domain): super().__init__(param, domain) - pybamm.citations.register("Kirk2020") + pybamm.citations.register("Kirk2021") def get_fundamental_variables(self): # The concentration is uniform throughout each particle, so we diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py index a624a23c10..2d80455c70 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py @@ -25,7 +25,7 @@ class FickianManySizeDistributions(BaseSizeDistribution): def __init__(self, param, domain): super().__init__(param, domain) - pybamm.citations.register("Kirk2020") + pybamm.citations.register("Kirk2021") def get_fundamental_variables(self): if self.domain == "Negative": From e0de928cb59915359afe8bcf5870f77ecc33dd36 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 15:48:22 +0100 Subject: [PATCH 44/67] added MPM notebook --- examples/notebooks/models/MPM.ipynb | 999 ++++++++++++++++++++++++++++ 1 file changed, 999 insertions(+) create mode 100644 examples/notebooks/models/MPM.ipynb diff --git a/examples/notebooks/models/MPM.ipynb b/examples/notebooks/models/MPM.ipynb new file mode 100644 index 0000000000..3ae0f50738 --- /dev/null +++ b/examples/notebooks/models/MPM.ipynb @@ -0,0 +1,999 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Many Particle Model (MPM) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Many Paticle Model (MPM) of a lithium-ion battery is an extension of the Single Particle Model to account for a continuous distribution of active particle sizes in each electrode $\\text{k}=\\text{n},\\text{p}$. Therefore, many of the same model assumptions hold, e.g., the transport in the electrolyte is instantaneous and hence the through-cell variation (in $x$) is neglected. The full set of assumptions and description of the particle size geometry is given in [[4]](#References). Note that the MPM in [[4]](#References) is for a half cell and the version implemented in PyBaMM is for a full cell and uses the notation and scaling given in [[??]](#References).\n", + "\n", + "\n", + "## Particle size geometry\n", + "\n", + "In this notebook we state the dimensional model first, and the dimensionless version at the end. In each electrode $\\text{k}=\\text{n},\\text{p}$, there are spherical particles of each radius $R_\\text{k}$ in the range $R_\\text{k,min}_v = \\int_{R_\\text{k,min}}^{R_\\text{k,max}} f_{\\text{k},v}(R_\\text{k})c_{\\text{s,k}}(t,R_\\text{k}, r_\\text{k})\\,\\text{d}R_\\text{k}\n", + "$$\n", + "\n", + "In particular, if the variance of the particle-size distribution $f_{\\text{k},a}$ is shrunk to zero and all particles become concentrated at its mean radius $\\bar{R}_{\\text{k},a}$, the variable `\"X-averaged negative particle concentration [mol.m-3]\"` will coincide with the same variable from an SPM with particle radius $R_\\text{k}=\\bar{R}_{\\text{k},a}$. However, `\"X-averaged negative particle concentration distribution [mol.m-3]\"` will remain \"particle-size dependent\"." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The convention of adding `\"distribution\"` to the end of a variable name to indicate particle-size dependence has been used for other variables, such as the interfacial current density:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "X-averaged negative electrode interfacial current density\n", + "X-averaged negative electrode interfacial current density [A.m-2]\n", + "X-averaged negative electrode interfacial current density distribution\n", + "X-averaged negative electrode interfacial current density distribution [A.m-2]\n", + "X-averaged negative electrode interfacial current density per volume [A.m-3]\n" + ] + } + ], + "source": [ + "model.variables.search(\"X-averaged negative electrode interfacial current density\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As the interfacial current density is a flux per unit area on the particle surface, the \"size averaging\" is done by area (to preserve the total flux of lithium):\n", + "$$\n", + "\\left_a = \\int_{R_\\text{k,min}}^{R_\\text{k,max}} f_{\\text{k},a}(R_\\text{k})j_{\\text{k}}(t,R_\\text{k})\\,\\text{d}R_\\text{k}\n", + "$$\n", + "The averaging is merely done to allow comparison to variables from other models with only a single size, and are not necessarily used within the MPM itself, or are physically meaningful.\n", + "\n", + "Note: not all variables have a \"distribution\" version, such as the potentials or temperature variables, as they do not vary with particle size in the MPM as implemented here." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Mesh points\n", + " By default, the size domain is discretized into 30 grid points on a uniform 1D mesh." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "negative electrode is of type Generator for Uniform1DSubMesh\n", + "separator is of type Generator for Uniform1DSubMesh\n", + "positive electrode is of type Generator for Uniform1DSubMesh\n", + "negative particle is of type Generator for Uniform1DSubMesh\n", + "positive particle is of type Generator for Uniform1DSubMesh\n", + "negative particle size is of type Generator for Uniform1DSubMesh\n", + "positive particle size is of type Generator for Uniform1DSubMesh\n", + "current collector is of type Generator for SubMesh0D\n", + "x_n has 20 mesh points\n", + "x_s has 20 mesh points\n", + "x_p has 20 mesh points\n", + "r_n has 30 mesh points\n", + "r_p has 30 mesh points\n", + "y has 10 mesh points\n", + "z has 10 mesh points\n", + "R_n has 30 mesh points\n", + "R_p has 30 mesh points\n" + ] + } + ], + "source": [ + "for k, t in model.default_submesh_types.items():\n", + " print(k,'is of type',t.__repr__())\n", + "for var, npts in model.default_var_pts.items():\n", + " print(var,'has',npts,'mesh points')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solve\n", + "Now solve the MPM with the default parameters and size distributions." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bf100c6314c74d7c9ca92ee5379f5324", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=3580.9687331142236, step=35.809687331142236)…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim = pybamm.Simulation(model)\n", + "sim.solve(t_eval=[0, 3600])\n", + "\n", + "# plot some variables that depend on R\n", + "output_variables = [\n", + " \"X-averaged negative particle surface concentration distribution\",\n", + " \"X-averaged positive particle surface concentration distribution\",\n", + " \"X-averaged positive electrode interfacial current density distribution\",\n", + " \"X-averaged negative area-weighted particle-size distribution\",\n", + " \"X-averaged positive area-weighted particle-size distribution\",\n", + " \"Terminal voltage [V]\",\n", + "]\n", + "\n", + "sim.plot(output_variables=output_variables)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also visualise the concentration within the particles. Note that we use the dimensionless radial coordinate $r_\\text{k}/R_\\text{k}$ which always lies in the range $0" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Concentrations as a function of t, r and R\n", + "c_s_n = sim.solution[\"X-averaged negative particle concentration distribution\"]\n", + "c_s_p = sim.solution[\"X-averaged positive particle concentration distribution\"]\n", + "\n", + "# dimensionless r_n, r_p\n", + "r_n = sim.solution[\"r_n\"].entries[:,0,0]\n", + "r_p = sim.solution[\"r_p\"].entries[:,0,0]\n", + "# dimensional R_n, R_p\n", + "R_n = sim.solution[\"Negative particle sizes [m]\"].entries[:,0]\n", + "R_p = sim.solution[\"Positive particle sizes [m]\"].entries[:,0]\n", + "t = sim.solution[\"Time [s]\"].entries\n", + "\n", + "\n", + "\n", + "def plot_concentrations(t):\n", + " f, axs = plt.subplots(1, 2 ,figsize=(10,3)) \n", + " plot_c_n = axs[0].pcolormesh(\n", + " R_n, r_n, c_s_n(r=r_n, R=R_n, t=t), vmin=0.15, vmax=0.8\n", + " )\n", + " plot_c_p = axs[1].pcolormesh(\n", + " R_p, r_p, c_s_p(r=r_p, R=R_p, t=t), vmin=0.6, vmax=0.95\n", + " )\n", + " axs[0].set_xlabel(r'$R_n$ [$\\mu$m]')\n", + " axs[1].set_xlabel(r'$R_p$ [$\\mu$m]')\n", + " axs[0].set_ylabel(r'$r_n / R_n$')\n", + " axs[1].set_ylabel(r'$r_p / R_p$')\n", + " axs[0].set_title('Concentration in negative particles')\n", + " axs[1].set_title('Concentration in positive particles')\n", + " plt.colorbar(plot_c_n, ax=axs[0])\n", + " plt.colorbar(plot_c_p, ax=axs[1])\n", + " \n", + " plt.show()\n", + " \n", + " \n", + "# initial time\n", + "plot_concentrations(t[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmUAAADnCAYAAABfaCHdAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAC/HUlEQVR4nO39aaBuS1UeCj/PXBwPqEgvAirGhqsmKlGCRDEiwqcYjUm+mKuxCcYE80VRY1DEmAQjdiEx6lUTMSCIvQnKvTYYkUYQRYkfaBCNDbHF6JFziMSIcta4P6pGV1Vzvs1ae+219q5xzrvX+85njmrmnDXmU6NGVVFEMGXKlClTpkyZMuX6ynK9CzBlypQpU6ZMmTJlkrIpU6ZMmTJlypRLIZOUTZkyZcqUKVOmXAKZpGzKlClTpkyZMuUSyCRlU6ZMmTJlypQpl0AmKZsyZcqUKVOmTLkEMknZFRaS70ryzSRPrkHabyb57ued7kUKyQ8j+cvXuxzHCMl/T/Kf7XHeS0j+/Yso05Qp10puJltG8rUkH7WB/wjJv3txJTof2fceknwUyd++qHJdNbmypIzk3yH5qvoQvKE+yI+83uUaCcl3Iykk73LGdP47ycfobxH5TRF5exG58+ylzFLT/fXzTvdaSr3G76m/ReRlIvJ/XM8y7SMkH0/y5fGYiPxDEfmy61WmKRcn05bdXLZMRP68iLwEAEg+leS3N/jjROQ516VwB8hF3sObSa4kKSP5+QC+FsBXALg/gHcF8E0APv46FutMclYjN+VqyrzvN7dMWzblqsm8v9dYRORKfQDcA8CbAXzCxjm3ohi6362frwVwa8UeBeC3AfwTAL8P4A0APj3o3g3AvwHwGwDeBODlAO5WsUcAeAWAOwC8BsCjgt5LAHwZgJ8E8EcA/jOA+1bsNwFILfebAfxlAI+v5/5bAH8I4GkA3gPAi+rv2wB8B4B71jSeC+AUwP+uaXwhgHer6d6lnvNAAP83gDcC+FUA/yCU76kAvhfAt9XyvRbAwzauoQB4z/r92QC+EcAPVd1XAniPFT0t09+t9b4NwD8N+ALgiwD8Wq3n9wK4d8A/rV77PwTwzwD8dwCPqdjDAfxUvf5vAPANAN6mYj9R8/1f9fr8n3qvK/5kAP+xKevXAfj68Fw9s6b7O/V+nKzU8akA/iOA76nX4+cAfEDAtX5/BOAXAfyNgLX3/T8B+BMAd9Zy3xGu+dOC3scDeDWA/1nT/ujw3P39cN7fA/A6ALcD+FEAD67HWfP8/ZrGLwD4C9e7Pd/MH0xbdlVs2RPqtX8DgCfteW/uC+AH6/V9I4CXAVgq9t8BPAbARwP4UwB/Vq/Da8L1//s1/TsQ2imA+9Xr9o7198ei2IU76v18/x3X4XMA/Hq9J08PZVq9X6HMTwbw8wDeAuC79riH9wbwrfXa3A7gB+JzG9J+IIod/AMArwfwOQF7OIBXodis/wHga653u73mduF6F+DgApcH+a1641fO+ZcAfhrAO9aH+BUAviw8EG+t59wC4GMA/DGAe1X8G2ujeBCAEwAfUhvHg+oD+zEoxOKx9ff9QkP6NQAPQTGGLwHwVRVLD2s99vhajicCuEvVec+a7q213D8B4GubhvGY8LttBD+B0su+K4CH1of80RV7KsrL/2Nqvb4SwE9vXMPWkP1hbSB3QWmw372ip2X6llqnD0BpxO9T8c+t9+adaz2/GcB3Vex9URr4IwG8DYB/jWKwlJR9EMrL5C41n9cB+LxRmcO9VlL24Hqf715/n6AY2UfU399fy/J2KM/NzwD4zJU6PrWW62+hPENPQjEmt1T8E1AMzYJCDv8XgAds3PfHA3h5k8ezUUlZve5vqs/GgvIsvnd47v5+/f7xKC+w96lpfwmAV1TsowD8FwD3RCFo76Nlmp9pyzBt2ZYt+y4Uu/B+tRyP2ePefCWAf1/vyy0APgwA27rXunx7k+9L4G36WQC+PGCfBeAF9ftfRCHjH1yvw9+tad+6cR1ejEKW3hXAfwv57HO/Xg3gXeDEftc9/CGUjuu96jX48PDcql1eUOzSP0ex+e+OQho/quI/BeBT6/e3R7XXN/Lnuhfg4AIDnwzg93ac82sAPib8/igA/z08EP8b2aj8PsrLfqnYBwzSfDKA5zbHfhTA363fXwLgSwL2j0LjSQ9rPfZ4AL+5ox5/HcD/P/xebQS1sdyJSjoq/pUAnl2/PxXACwP2vgD+90berSH7DwH7GAC/tKKnZXrncOxnAHxi/f46AB8ZsAegEJy71Ib5XQF7W5Se5GNW8vo8AN8/KnO417FH9nIAn1a/PxbAr9Xv90chjncL534SgBev5PtUhJdAfW7eAODDVs5/NYCPX7vv2E3KvhnAv11J+yVww/ojAD6jKdcfoxDSR6MY4Ueg9o7n5/p+MG3ZVbFl7x2O/SsAz9zj3vxLAM9HsEejumM3KXsMqp2qv38SbsP+HSoJDPgvo5Kflevw0c19/fED7tffW6vH4B4+AMWTdq9B2o+Ck7IPbp8dAE8B8K31+08A+FJUT+3N8LmKMWV/COC+O8a1H4jislf5jXrM0hCRt4bff4zCwu+L0jP7tUGaDwbwCSTv0A+KR+cB4ZzfG6S5Jb8Vf5C8P8nvJvk7JP8ngG+vZdpHHgjgjSLyR+HYb6D0itfKd9cD4gMOrdva+Q8G8P3hGr4OxQDfv9bBromI/DHK/QYAkHwIyR8k+Xv1+nwF9r8+APCdKGQLAP5O/a1lugXAG0K5vhmlB7wmsZynKMNID6zl/DSSrw5p/YWmnOm+7yHvgvEz2cqDAXxdyPeNKF6xB4nIi1CGe78RwO+TfAbJdziwHFPOV6YtG8tls2WxbvH6b92bp6N4rf8zyV8n+UV7lq2VFwN4W5IfTPLdULyG31+xBwP4J819fBfk52Ovuux5vw6xW++Ccg9v33HegwE8sKnDF6O8DwDgM1A8tr9E8mdJfuwBZbiSchVJ2U+heDX++sY5v4tys1XetR7bJbehuMXfY4D9Fkrv8p7h83Yi8lV7pCt7Hv+Keuz9ROQdAHwKykt1VzpAqd+9Sd49HHtXlPioyyS/BeBxzXW8q4j8Doq36Z31RJJ3A3CfoPvvAPwSgPeq1+eLka/PLvk+AI8i+c4A/gaclP0WyjN131CmdxCRP7+R1ruEci613L9L8sEoQ7efDeA+InJPAP8V2/dx675q+UbP5Oi8z2yu7d1E5BUAICJfLyIfhOJZeAiAL9gjzSnXTqYtG8tls2XvEr7H6796b0Tkj0Tkn4jIuwP4awA+n+RHDtLebPtSZjJ+L0pn8pMA/GAgq7+FMrQZ7+Pbish3HVGXXfdrVNatsv8Wyj2858Y5et7rmzrcXUQ+BgBE5FdE5JNQOshfDeA/kny7HWleablypExE3oQyzPWNJP86ybcleQvJx5H8V/W07wLwJSTvR/K+9fxvX0szpH2KMob/NSQfSPKE5F8meWvV/ziSH1WP37Wut/LO26kCKHEIpyjj5Vtyd5SYqjeRfBD6l+b/WEtDRH4LJabhK2vZ3h+ll7Gz3hcs/x7Al1fygnqPPr5i/xHlGn8IybdBce1Hw3B3lIDPN5N8bwD/vybt1esDACLyByhDA9+KYgheV4+/ASWY+d+QfAeSC8n3IPnhG/X4IJJ/s/bOPw/l5frTKLEngnLPQfLTUTxlW/I/ALxzrfNIngng00l+ZC3bg2r9W/n3AJ5C8s/XvO9B8hPq979Ue9u3oMS4/QnKMznlOsm0ZVfGlv2zem/+PIBPR4mTAjbuDcmPJfmeJIkSD3onxu3tfwB4t9qxW5PvRIlN/WR4RxIonb9/WNs1Sb4dyb/akNlWvoDkvUi+C0p8r9Zl1/0aydY9fANKOMU31fxuIflXBqf+DIA/Ivlkknerz+NfIPmXAIDkp5C8X32e76g6N7TdunKkDABE5N8A+HyUQOY/QGHbnw3gB+opT0OZsfHzKLPMfq4e20eeVHV+FmX456tRYnB+CyWQ+otDnl+APa5hHYb7cgA/WV20j1g59UsBfCBKI/4hAM9r8K9EMQJ3kHzSQP+TUMb1fxfFxf0vROSFu8p3wfJ1KLOq/jPJP0IhMh8MACLyWpRg4e9G8Zq9GSVG5i1V90kow45/hGKQvgdZngrgOfX6/O2V/L8TJU7jO5vjn4YSaPqLKDOF/iPycE4rz0cxlLcD+FQAf1NE/kxEfhFlxttPoRit90OJA9mSF6HMIPs9kre1oIj8DMrL4N+iPBsvRe6h63nfj/K8fncdgvivAB5X4XdAuWa3w2e3Pn1HuaZcY5m27ErYspeiDEX+OIB/LSL/uR7fujfvBeCFKDbspwB8k4i8eJD299W/f0jy50aZi8grUTpSD0QhOnr8VQD+AUpYwu21jI/fUZfnowTWvxrlvjyzHt91v0ay6x5+Kkq88C+h2PHPG9TtTpQZpA9FmSx1G4D/gDIzGSiTYV5L8s0o745PFJH/vUfZrqzobJApUy6dkHx7lN7Re4nI669zcUxIPhUlgPdTrndZpkyZcm2EJYbr9Sizqt+64/RLLyQFxZb+6vUuy5R1uZKesik3rpD8uDpU8HYoS2L8AsosnylTpkyZMuWGlknKplw2+Xj4YozvheKunu7cKVOmTJlyw8scvpwyZcqFCMnHo6yOLgCeKCI/F7B3R1lD6rTinyoic9PiKVOmXArZYb/uD+A5KIvv/iaAJ4jIW0g+G2UB9TcB+AMR+YSd+UxSNmXKlGstJO+FEij9CJT1pp4rIo8M+L8G8Asi8pxq/N5HRJ58XQo7ZcqUKUH2sF9fi7Kg+HeTfDLKGm3fUknZfxCRl++b15XfWPSWW+8pd33bdzrnVA9Z+uraJ3P+SW6kQuTVZ84hwzbJhG2mf40u4Er9+tyuQeW5Dm2pZey4ct3xB//1NhG53yE6H7S8nfxPuXMV/1W85bUoy2uoPENEnjE49eEAXiYifwrg9STvTvJWEdGZta9F2QIKKNuy/P4h5bzKcg+eyDvilutdjCk3slwDU3rNZMP4/aq85SAbtst+AXvbsF326yEos2CBsszHP0CZ7Q6UZWneAuAbRKRdMaCTK0/K7vq274QP/IhnHaQjImDLBuLLcoUpDPUUi3rqfQznrukBnmbvtFzPr6S52Hm5nFv5AVxWsD31Ru3F9XqUy7IGgSdcxzbKstQ0R47eZbWc4XoqGO/7sl6W5eSk5jeon90/ve+jcko6t6QZsCbZqCciKc2TpZYF0l2ArOdlIYH/9HXv+Rs4UP4n7sTX3e3dVvG/+r9/+U9E5GF7JHUflGn7Kneg7MH3hvr7hQB+lORnoAwBPPzQsl5VeUfcgq+9S7fCyZQphwnhdqI1U0Rv+w7RIyCnrZEKaa5hBDpOdFLskQi6Fcd4UvQEAO7MafIuwEff8UsH2bBd9gvY24btsl+/gLJ8xzegbN1173r8SSJyG8l7A/hxkj8rIr++ldGVJ2XnJcd6H+wBygdrmkWGJGaYVkRlSDgADrxLuxZe1rRZXuR9ikWLMixYaXiOcZCFF52B7zD9TWkTSFeJg1I31zGWR42BNe6Q5hqWSOcSwFo/01vYkCj2mOi9d70Fi+st2xhDmqCSNvE6069dS+gsTdLIlxJKhmtWyingsmwS3C0hieUu59LNfiPcEwaUdYjeGH5/Ncp+i88j+UkoK4x/1nlkPGXKDS3Dl4n+CbZXz1uQOqWdbQidSyVWQLVpp55GxHBCJ2YRYyFTcmf9vng5rA+spO0k2rZSgNO3lvcOT3iUDbtA+/UVAL6B5N8E8Br4zg631b9vJPljKPFlk5TtI1tesH30DtV0J0ZsLVHWju8WMjnpmlQ5JqAEIFt5NljPlHqIK+fWcqAhgSkFkRXiyq6CXV1HmBqIeHIEGyySIpAN5qVN5CocHWKqRyWWse7x+rK5LrlcvV7IM10LtaZnMEoLcHK3k3X8j9ahRl4J4GksOwo8AMCbg+sfKIXVhXN/H97TnDJlyqZwu6mPMNYDIxst1cYY1tiwxXWyuWHGsqECV/T6NB09eZua5rGvw132C9jXhm3ar7o7x6cCAMmvAPBj9fs9ReQOlt1aPhRlMsCmTFIWRCCHe8yqZ+jQ58W0DlBMnp+DcqxemzHk0gxrGsFYJVf1arUEioSwXkvJjaxwMU2zSTAck5qOJ8nh31Fd1oemB4Q0Yg25GhO48r2tU/yxikUPH9gbyXHB+svUGEmS4+FYNbqno/T3FAK85QykroqI3E7ym1BWRxcAn0vyoQAeKyJPR1kJ/ZtJvhVlc/jPPHOmU6bc4CIEypqwK3aNycQVWbSzGQ6O+4OdHhpbY+cu0AEH17FRifpqOY1Y1KtpNtXgCcoIQNQ7VC7IfpF8NIB/hlLaHxeRH66q31MXQb8FwLfXXWs25aYkZVsv9C1ituVJkw18TIb84R7xEz1lXAYJ+EbXqNWLD33XVQEYvYVhaK1IvC5sejSZuEUPkdAHJc0QaBUooZLM9WHjA6pDfnp/8rm1hOq1bK8L/b4SjR6rXpNmGWLsr5Fdy2iAQj6rMWsEFi5Jz6serlczZm1DtUQYNnW9Ej9XE5UVvaWPWdtXuBAndzuf5QxF5Fko+zFGeXXFXgvgka3OlClTVoTRFkggWqjEqz0Z7gGLXjI9o5Ku9N7Rd0bUiTYMAE6askTHwRLzRLZDJzB7bWZPPM1l0UTFSduBcoH260Uo2+W1Oh91aD43JSnbNVS5hp+73g4iuJ6fvsDHxG3Vc6f5jXpDADbHPYXDhhzLPyyrOgQbV3ZpoMyGJOlJIWLRKxWJIPv8SI2/YpefErohRid7Sv5aPYL9BImYzGjyhIZYnNAuuhJMvS5Lp0cL0F/aazbAnEh6mVf1jhASOLl1rjE9ZcqFy9AuIg315b+SRzsCThHgLswvByqGEhM20IMAXNGDoMR6bejlji1q57LX44oet95Je8hVtF83JSkbinkv1hjLmp67PdYImzkzgkqMMV8neo1iA6165kazKGOb6zCuYyrLCCu/l2W97jwAi7xhQQhOtz+0emc911zY6CXvUSRPTF4p0kmXBtfHodJIuuJsSJq3KmAxTQ2kYA3ID3VwvTypYyF9xiolzXoq12yp6QtOT/329XpIekcJK6mcMmXK+QnDp500tYR3UIsRgXgFdxXhwfBKfIKXiwvNeyaq2uiR6GyN6TZ6aksNCyEsllfATK/qjGZ0pvwY0tR33TFm6Arar0nKgpxlhtqaI2KUomczaJHYjalnZxMbZNxjmYytLSTc5deSOAlpGSQ1zREWrnVMy/7mcnYB+A1XTbM8I1mNRk8NSRouZTZOkYQFYtaSR42V69KkErp6PHrlqhtelyuJadrsV/V8BtJJElgKwSpcLGPL4rMvM1mNesfNXNJyL7fsCJSdMmXKfjJqhtqPAv17bK/VMMSmjWoniueMNqvRbQMSEYp6i2JLCOivestJtHuaX5NmmF2p+S0Mdq/Lb4TVNE+ynTWolkmxfkRhT7mC9muSsipnIWRDSUOPmUWsDTs65g9n3zb9RZ6dwzk+Cw22VpZYh84D1XqLGq/Wajk3MD24K2CfMh6ebI3BWj2SmJdvtD5dSVPYY+Z1G+WnxwZ6kahljN7DBLL3SoLeID+ebKS56LXJmDTYMUJcvZ7mlCmXUhpvj5EPIBCt8DGo1Qu/1SNVdZJtaOO5Ysey0Yvpl8Mc6iWvVZNfqxfrxw09i/YdpbkoIc3XYF+5ivZrkrIqOiS1FuS/rTciFoUEAP3DtCzAilOqdH5WkwyxT8P8NIOWJCwhJqBZhqL1AKVyNnoRI/PkhqZxpkrEPCpLK0luzNpsy9YQklTWBTajUyAec2HnNzEKJQGoh1MJbsJsHbOe7Pg6ZswEE1GvzU/P8xkAmbhGLJdz4ZI8lT1RdrQbcj3rNmokTm65WjEZU6ZcBkkxwQSgk560rSsWhvqcxAQsmwYjdzY7MXn76zkn1dOlAfKBfMVhx+jxL3rhfdFiVS+tC6n2ZilhLJL0ajkV04D9QPRoaYrNErc0TwAuJ/0Ep0PkCtqvm5KUrXlnANiDs+WJaQ7a1y4wP52fsa0HzDEm71hbBsE4P40laGfjFCyQGrMDLYnLhEfPShgZuI+TEjc0ekElYZnchBQa0pVmbbZ6sezq8l4G9TIPEV0zGr1Qh+Td05mXi6NGFGs+tnJ+g7V6aDGO02wxFXXbL9wvzXrjS7A/gVzDw4WE77owZcqUvWX9PVM6xgJxgqSEqfM0BS93JFdLmJ0YbDz1b/BKtaSs04PmFWwKYvdR01zsPInvmEXtTS2rOjhqHpofY6phRMBsflq0NuZ3tolKV81+3ZSkbNcsyjUseYY2tts5KM3BMF3S2xhLj6QmAxt6jC94pIYJODZqA924fnIzr5Wln7XILb1AFM1YSa93spxU4xA8nAxpqnFo8zsJWz4pYauqy7JUtTZNYjlZx7ihdxK3UkplYdpmyUgrBvmlSx3T9Ou1T35HCYHlLlcrJmPKlOsiIw7WeMDs70nG0tDeSdsJdT31jrlnHLa4qk64kqQIJ1vmoWvIn+bXbBNHjMrCUJacH8M/nV70/KueTQAI56nnDFKdC7R0j5qsdAXt101JykbiPH5MtrqjYciq6I1TXUfXMeN9W+UcGQBNcYhtKFi+TA1yVRoSuTXe33q5YuJDLJwTXfjRe0kyB8QGcpLqsPRYm2bGKrR4YKpitgTFosGnfZp7YYPrsrTYwuAhW58l2o5ORj0O9I4RXkH3/5QpFy6hM2m/lfTQcQAhwD4Qo6rjMxAbHSixCvp6inrGRuuMARZXiphnmARgtq/tPMfJQ2pK4+QBamhHjyU9tpjbPxvujPUja8gGylIeIb9D5Srar0nKqmxuNn7sVkqbHrkYrzTCazD4Cplq27lj23rpb1LbqJ2zjKFeuTYr+a0UNDbaYVkiKQtpWH6xLBzrcUuvwfLfUOVuVqNibkCG2IJNvX0xL4s/K6Nh8G7vy6B39MwlzWPNCzplypQilXRYSElLyKKtWQaYEhYlZSENSCVpI72aJiOGqBfLkm1f1mOnl8qi2Wma7WzPgCU7rGWyNAd6p7A1KTv7LWpLu9Vw95arZr8mKatyzNDjWdLcfsK2CNJmhsclOVpwcA9FG3ocEK/O5T3ITwPsY1YppixCiaj0HrZoVHLcGo7T44Ze6o22cXJeo+jV8nNjGjG/Rm81zea50t8xuLZKJGPHPsO4gj3NKVMuXGIDjYu7duSpkiTdsiiQt0h2kt5ohmUkc2qPWtvQbgw+sBtDUmZpstNLRKtLsyVV7Mo4ShMnWiCM9ewC7X87Ypmumv26KUnZ6AW1tu5XwbZ1gbXnZZ/oxBGjQQ7OTBh9ZmNblg09IqxM3xIh8VmPyf1ev6gb2Q+tN5C2x7XmkbIFWNuGGNLpSFFNc+TxSvWL3qKGmGQS1lyZ5GViuNQMRBK+d9xQz+vX5rcQfv/QYEu+aDFNx4A4YaTE3cFKsq13nLTXcMqUKS55og8SyQKg60SkzqGREiVE1YNk4VWRLNUhvTUsmetAztTzpFvIRTKn2ylhhIV4r9bjpQH7tnZ2JGfLUpPME5HKJICKYVSWGDsbsMW3pYt6h8pVtF83JSnbJfsMZfbHe450pjS5hfEovRg3lx5yNlinh0QCEyFKxYpGgtagFOt6XCM9ejmSnpYx9ELjbyWUuuhs1kPkO0ZmvBIEWUhnq8ew3AaA5OWzq1YN0xLKqfFsmo97reJ1zBjNoq5hNecG8/Js6R0pvHqBslOmXJT0ndgElvbYzoY0IsOwlpi2a3HP2BZGx4qIExDVAUJcLbNe7SUybmcXhiudaNLKb51Yot8tQI+DJYgsTD4wjKyd2EiWgp7KQsSZl/pGOI6VXT37dVOSsmNnX25hHCwimvF1F2rrVk7Yxnj4Vg9gnxmWCTNStK7XkrHs6RmXc+HSz77Ehh5dLw2PhvzIQZxU0NNrZlVT/RN219PPWXKagcyV/HwJjKSvWHsL6eV0sxlIU/C+5fTa+oVp4ij3PC6PETsDeburPGvz+LiycTmnTLlphfl7nkUJJysnzN6sQMx4F3pgO1wHpOkRPbacRHsaRxuKHeraqtm+xdKME88BltmQiyYUYuKAkJ+OJyBj9dy4dhkAK4uNQ6Q0Q36NN4OWpmIw/Dg7dPXs101JyoYiyuQHQ46VxK0NRuqDPsTU9zwGm+lzWW8rza1tlvJ4a4M1x/y0Jr8NvTUv1xBDJmGj89S7NDJeI8waLUJ7tt5gJTYVNCMUsJi/2gSb3BPy06HLZVUv5EevqZIvJUnRkEFtUIuF5XssP+b7QtLLWX/nPVRzWeK1PtZhRl69nuaUKddMzJRlG4XQLo2oRSwSt7qwawrOFySPmrX/YHvbmLNo61N+aGx27QCmWdu1qIt6wcyzvhSbsnjHMWFV02yblZdlH8taTk2TqDMpRdzjloL6l05PjdgQO/R2XUH7NUlZlaM8ZxtbKcXYnxaqPowRN0r59SlmohNTkI38NusQ04yGZUd+psdAlEb5bZSliw3bI79oBxO29GXRcxYyuOCz5qJTtNsyR2xUPyNjfR0WBhd9hpoZSDHBUuclpBsggEoc22Fg/9vq5eM4Tnj1AmWnTLnmEpas6NrxSSQdDI0fxUNGJVcNcTuJx0J8GHakeQIjiV0nOemF9FA9VgM9MnvB2rAU1jUUE2lEtW0natzUVmmadV1Gok/zREciBvnZ6Eau195yBe3XJGVVLPB8FRuA1sXpMfViDNVWMypJrY2eq4dspG+9pmExW2aQy9Ks39fpjTxsXYNt0lSS0TZcTSf1NAdpEg3ZCY08YYAF3tOMWSYtIUJihdCwI2XqbRqSMh2tHGAaXD8iSclj1WBlmDMuk9Km6detx7b0HDtWrtqU8ilTzlNam2sdQCCTqtFyFka4iLhPJaM33DgHQ1pIadnf6DEKdggItijZy/w7ldPsc7D1TR2SfbPh1l4PrSeLNHtna5rZ9WLKj6kSIc2TGlcmXodj5LzsF8nHA3gCilF9ooj8XMDuD+A5AG4F8JsAniAibyH5bgCeVY//kIh8xa58bkpSNvQabd3wrTdaJUNG7luSoS/F1UzYfI2xBMx/Nc1Ivpr80mwgcBuzdkAfxwsMrTsnHGt7Tx1Wf3d7NtKPj/S0nLERJuOidV7BolcoYTLC/AKWmMCKLd6DtOHGaIRiHUKaS2OYotFalkav5mN6qiCalqdp9692HBaOsC09HC0ksNxlkrIpN6+YHSXSDEsAaeHWbrskxeoq/K7LtNArg9eMNU1AhzjHts9tkWLuVFjbuknTidssqQl0bFnVY4id7TE3hJZmNVbD/Co20lMj19rvo+7dOdkvkvcC8DkAHgHgQQCeC+CR4ZSnAHi2iHw3yScD+DQA3wLgqwD8CxF5GckXknyeiPzSVl43LinbmKyxK9D/6JmSQy8Wq56/vHMxVzYYN3xQjY386KDPVIxYbNwJjKwGfZnon1E6I6+YDqmqgcrZOWkYDWFqeosxoqZBH4otLYZOj6yGU2uv9iL2+OA6i9XZZxnlS1nSWU7o9zHmD+DkhGFWKNKs0JMTdIH8US9j2CPNY4QX1dP8fAB/rf58MIDnicg/OZeMp0w5q3jz7o8tG9gJMpEL58cFYZOOcpgmLs2xYE+DrbG9NBvvmuZpHdHw1/RDPNcIS96tFgud2NSRP2Gy76onO/XcBRg9c3JU7/Lc7NfDAbxMRP4UwOtJ3p3krSLyloo/BMA31O8/A+AfoJCyh4rIy+rxHwLw4QBuUlI2Eq58h5OQ1SUhkGOsRugW0Vsjcvl7r9+XhatYVHesL29Hgraw8NdmZgZDoH/bPd42SRd6PUaMh2A8QA8Zk6C/xC2KPJBff5unq8UWn2HZYsuCnGZTlqUpi5GoBuu2fFrBlk09HCfkuQTK7uppisjXAPiaeu4PA/i+M2c6Zcqh0tgoO9auQaakZ2Vh14gp+TK9EwBGWGDtPhGu4CVKQ4lxeQ1kO2TnDPRSTG3MLxCugoW1xAYYEMviRC4aMPXU+cgHUppODMM7qtFLaQLVS3eEEdvPft2X5KvC72eIyDOac+4D4Pbw+w4A9wbwhvr7FwB8NAox+5iKAdkdcQeAd9pVmJuLlG15z5RUDSdfHr5u2R4Z9hADdEx+oxmk+6QZscFLnGo5LMnGKCT7lbFWszUmSTOl2WiuYllvNMMyY4EIJaKT9drv0ZgZNw35sSa6dGkGPSLM6JSgN8a0nIotsZyqu1RsQTMzs/xV7GhSFhMbyz4GDdjd06xZ8R0B/DkR+ekzlHjKlOPE4nrzMg5rhMwM3IAEJaymMdrD0W0A4YvNqp2KJGiQXzjHyGOnt5GmpmV6S69n5y917UW60QszMwFkT10dPVgqwbL6wTEqttRCWJqsaZae5Zm8XbuN320i8rAd57wRwD3D73vUYypfAeAbSP5NAK8B8Lv1+OmGzlBuGlIWPWCs/yXZuG8HecA0P9nANsqyRZ5SfpmzZK/PRprGaXKiuYFnKLzpkf52vbIBxrbMK/mxS5MDDBsYbR2evg4rmNqVxus2rMOyrtcPh4YNzFM5xWZ05nMC1uijYicWENsG/ov3PiMG1wtc+GAhd04p38egAbt7miqfCOB7DynjlCnnJqHtZwIUvrcxW+HcHDsG7yQuIz1P10mQttecvnuYQn7INlj1IikaDmfCyVNO0wnTLj20GL0syUYbtuQ81jC26S/p98G3c7f92ldeCeBpJG8B8AAAb44dShF5E4BPrXX7CgA/VqHXkPwQEXkFgMcB+LxdGd00pKxzRx9wg7dHstfWCzuuLPsWqyOWRP6+mmYPdgRVodgYtdyt0WqTRiUpVIIRsrNG5idHIhRXfy4YdmK6We1qmk05E8GLAf1AIl5G1A7QY8iPlk7Qs3J62YHaqa7bjXRY0PP1yjztiBEInjpPc2sLsZ3C8xm+xO6epsonA/iU88hwypR9xeJzNRZXZy4bcVGiU9p72hVFyUUlZB4/GskFgCUupMqUZsGQyJ+muSw6+YmmVnuYFXN7HAnZsiwWh5U60JV06aoCTHpLxQC1t63XzYYjFzRpLr5M1DA/f4e0eubEUExJ47Lk63monJP9EpHbSX4TgJei3OLPJflQAI8VkaeTfDSAf4biGftxEfnhqvoUAM8k+TYAfkREXrcrrxuXlO24f2vDgFteqi29i8Daczo9rmDMtCvta9YSrZiP4vV72lsy6Oo5hBswNvreUA3wPNjr6Tm7sQGe9HIdI2bnwG0kwECk/KrZ+Uusm6ZXsKXJj1HP0s9/CZrXPhVzoLewvjO6/Pzea5qRDO5sDDvknFbE3uxpAgDJhwAQEfmV88hwypR9xexcHOoP3hxQ+rXCVI+oGIKN09Xvi15H7gxbUnrJPoX8fFJRTevEverW79RzwxpjuhNJIToF098+cSlglh4RhxyTntUvYHZ88QleStSCndN9f3Gy2KxMHY3wci61fsz1O/beno/9gog8C2V5iyivrtiLALxooPPrAD7ikHwulJRxe/bVuwN4NgrTFACfKiK/fWxex26ldC22YOqw5rRDtlLKRGErTWYo6nVDcf7Ur217ZJjZjmycMobQSDPm5Mn1yLgSR9ZzA9mUUw1P6LFqWU509k4Inle9k5PGWxXKqdgoQN7SXNOLl4zosDj70rFq+ob5sZZFOuwuhnn9XA8g4vZLbkSPEZLgBfQ062mfAuA7zpzZNZKLtF9TroOEJSu0QwfClm4gkbxDRsZ0SBIIekyYLnthaYShzLH3KJMttvmlzi6MgK2maVs3begVJJDHZlsns3lsyJ+uhah5LoaJ6tnFgZM1TcrKoltFLVUv2PxK3g6V87JfFykXRsq4e52PfwTgmSLynGr8ngjgyedahiP5tmqtDFRuomuna1lkUCRLMXGnen5wR3cKlrh0emk/HsXqd4GkmLScZj43pYmexGZsRa+ShEhsFuZFbLMXSw1JwNS4YAE49ia6kYhpZk9XiwFtXFZYf4yB6zFigfjJGGODxQkCIZ7Veqlqd+N1KR8xz5rreT5Lvfd1JAHk6VHGLMpZ9VW2epoV/+fnktE1kMtgv6ZcA2H+7jYx/DaipacHG6zHwzlOrLSdN2moHYpEI2Ha1unl0zSjXjBGw/wQ0+n1zFu2NHVVPcUWT8PslM4eWgidHDDCFhJS9YBQB4ufK4o6VNnqUYdYzzBb6bzs10XJRXrKds2+ei085uReAH5/LSGST0DpseLWu91/7wLs8oIdo3fs7Etfw2vlDMN6dDO/SlhW9biCBeIx0usMyJreEMuGp2BLh3FDr8WcTMHOS/uxGYaAIelpmssoTQz0IsFq8lNS5lgkWRkrdehnXwJIWzT1+ZV7sQzqdxJi0JDSrDM2caTw3GLKrrqcm/0Csg273w0cRXL5JRCRcMgIkhEWP+6ki9tY9Y5JbM8x2B95lqcN+SlB0jIFvWSHweR1i0QudlK7dcZUj7EOjd7S6nGgx6zHgV5zjh1Lwa9a/mWQZsYOv71Xz35dpDXYNfvqhQB+lORnoGxJ8PC1hOqU+2cAwN3v9d77j8sQ2FymYlVv/XxamqsZZtEhp9jTGeU1xDAkVFYlJRYjvZU6pIY6wLb01jyPm2kW90+0YcP8IkZNU+vXEjYjjRlSzILvG2OnvUqir6stZ1Gzs4VbqTO2A5btS9CTBsvEj3tiy+B3LMtIDyhes9bLeLAQvevw5pRzs19AtmHvxbueYSbGlGOlkKXwPoh2w4gTO50lxHqljxKkaG+06XRYICbNGmJG7gZ67RDrWppGjIAmTeY00etZhzgSKTARro5kNUQrpxOwlB9taFivc4vZArM13YPlCtqviyRlu2ZffTWALxGR55H8JJR1Pz7r2MxWPVv1312EYqx3DhIbehyXas8xorVvmlVNh8g4OGHlZ/zNmNhqdv053fVk81WJTDQUsexGuMJx+04jcjEp1evC4GKaIbysxXSfyuJh7LF4WwxbfGjRCXbBloTpzCZWzG+1DutGrE3TvGELOk9XnpkZ01Q93f6JCTtGCGI5uVo9zWskF2q/ppy/5MlPoU1ouIe1uQHhopKZ8l2s7dGwpc6itLTD0GCKuW3I15IIEoxwgTF21vW0fMvicVtm6+jEiVRzkknUaLkLO65GRTzI34cc3SgmzxgX14t1WAhUjAw7kMQyJow26zKe4zMzDzdiV9F+XSQp2zX7igBuq99/H74i7jWRix7KPHrrpgP1OMI41rOGZd2yxp0+8nTFHl0sJ7bSLL/dG5SJWdbL5TVjUX50emlV/PQ36gHLpl4YMkQkZFreTHIMW7TeNc1qSNXwlPpI0osfQNJwpZHSqqeLxcKT8+/MHrqoVzDx8hOIcYYHCXHlAmWvkVwq+zXlcEm2kit/R06V2L4a4uEEDLANuwOxUrIUdTNBCgQHjV4723OFWC0RU+9Sa9dkf0JW6rMEfZZZomZP9LyqN8IghcQtS84HBFgxDrCgF68FUtf6kJt+9ezXhZGyPWZfPQ3AN5N8K4BbAHzmGfPryYx1iMIbbi8970m1mIQ0V0lebJC1IHXCdG2/6+XcP83gDUKjFz30RJ7tmbDsZk6L3JJYLMLc84mGInmbQhl1vSxG3dDYDdNUmcvi98uxSKo4wpTo1JMYyslYTo7TjGmTyyC/AaYet1A/36+yX6XfFovt8iROFrH0kIidZD2LI3QMCYNhx8p5TSm/ynLR9mvKNRR9v4fH2sgTgDij24PQy/lC77SCApwwLKMRp0OX5S7SEjrR/tlsSMXYYw15AhVbjAAZEQLAu2h+tGI4RseguuWk5US9YKUwkXCW2ZeLpxVtfcU8vZDfyYnrYYwBKMH8EVtO1tPkceTqqtmvC40w3bHOx2uRZzNdEznGO6Z6q5FjG2kWPYkH9tY7jzTjz9YFHM+Nnq0OCw2xPTcaDEOZ9dTwgNkIMZCLVq8NlorY1pZIaaakGaZIvpz46PXp08Qwv4iPMaZz0gSjDpP1NGM5kcvazvYsfVJNkyAlYKjrmknaBP0gIcEr5v6/VnIZ7NeUc5Joyxb/3XqQOiz0uth41VrPU8wrE6x1zIdOmbEmniuRoxToPyhLTCt02NNyHrEDjOg5a8qC4sEzIzXSS+dnLOuFsrTDlqGHr8Og6brtfZ+vnv26qab9nIWQ7cLatpY2rUbooUBDyXanmckO0rIVW2m2ZdmrDgQovWeu02OPWVnosBkfMyCub6SrFpYhXTWEaguQ/nocWKxrxuqxSFyMkLmnzK/TWppyICFr9LgLG9Whs3UB8y2a4uMBWDzs4L5LwM4QFXnFAmWnTNlb4qOttiHYqvgjNq247li0KY55e0wjM+6Q6rAtPSdUoZ0HjMHg5TRbj5sXNOqx1j8SIbPFrS7dc9bqIRFHL3fBqocvTgJQ/c54hzK3hvZQuWL266YiZWVbibZ57at3mI56s1Je4WV6TEzbrjQ3FPNfOKFKxK/5a43ZeoeuN8Zcb2hE1jCsYIHlueFjV520NEVjzJZQ7rYsTnIy2VHMSyaD/JB0o15ML3WYmzIkWx1tUXddBnFptVxm/5qyMGBo6naIkFevpzllykha28mFpaMbvWEo3z10AslG6pZI8VxQZ3ISy0k5qO3OT6sEJ9hLw+heKaYGHTG3f/rJnrVgP2v541Bm/HChLxKe9AicKMY+zXYoMxA+cElpIpUl6kWsprEszQoCioXh0SON2FW0XzcsKRsSnnooR3TtoxePeTBCPpfJO9bjOYhhDUv6YLeo62qaTOYm7yXWkJtwWiZncMPV6rIt+xALnigrzx5YIFQIpMtJSutV6j1f+RoV3SWVU+uFpONqnn/eoki68z0/J2sJq1fS8gNtFiiqGmETtQxjwDSdJRpy05NAxMLtpIAUXR7Jyu91OU6uWkzGlCkjyTbMj2ufW+1GtCU+KQq27pjD0W6jDme6PY12tLOhDRbLR7WP2qujjopIV86Uhh6PMyZZO/Kx42oTEmpITpwJGQLzRdYxQO0CkYY6QRCyoce8HIbWIQ4N2LF2Lcvj7NBVs183LCnb5Ylac37tu4rZoc/HVrpxC56D0uSGHleOoyFVA2ztuu2FDeDOeITydXr2tydcZowANzLI5MQNT0gupKFeLid2ll0iaTG9rBfzY7SbXrb6j35P9gawIQyNK7NyG+bDjr2eY6FzWc+TGj82xo4SsuzdNGXKjSCtXQxtd4gthK9q36bDtExGl4bqocEIWyvMbFM4XrClx6oxKbMTc3pKtmyvy0gAlRiFWexa3qLny1Z4PWgzK93jxlQO2PZM4UVUjDNwsoD0CQnJUC95ggBSmWizOTu9NohvH7mC9uuGJWUj0Qc1zio0THsNA7bG8MX1evbRExb9vbF0RdBbI0ldIQCkrYU29EYkqsMaQjPMl+t60XiY9yy1tYDBjWBpnznNqGezKLs0aUtJgD25irMvO71VUkacnPRliGkaOUr2ogxZsDnfbNcJO8zTjHrFy6XetWWRRq/QLCVdWg7HNLZMVrGjhLhy7v8pU7bE7CURGipsKYlI0mjH6TFf+vcEFtCZQiDCYrBp+C8RoeixClgkUC3WEaiKVYLne18Gm0rFGLBoOz1NI0WKLQ2mestAr0nTyGMqCy3Yv1uhn0o2GyzqHUXKrp79uqlI2T5rfq2+uw4iY5ZofvBG+ZEY5bru5TvDVkr2cPdVcbc2h1j0FI30MkEb65mxQzGMSxPMEfV2p+nEy06L+TFf+qQHa+sB64mTftfFXVt8qYY6kiRVVRvSY2IYO4wbenGCgIzz4yjN7JE7XIi0hMqUKVdcYmxZIluBjDlBUtKjxxrM2t1ou6R1QpbySYRsC1uS/qoeRphXsE0TlUStYkbWdmCabwrMH+uRsUyAkTKtX+r5+rplh8vVs183FSnbGs7cwrYehqP1NlwX1yLNZePBPC8st7ElFzUYjMXW2RkQHXXb6+nMWCSG3MDMqDVY0wHrSaPVJawHxv30GPU6TGpZvFwjrPMYRj003j3DYmxbjx1jyvxC4Mq5/6dM2RIbTiw/EBuIHR6SJ/3bY0ogUue2JVYNZuESHcaMNV61ESFLHfy9sDxkuYlxDwwIsWPraUYiF0nXGNPPGWZfXkH7dVORsrS2V4cV4eDm55f1YTkOhfQtloBkFDZFbUk0KBGMfyN5QZ61aUZpgJkOOchGjYkeDYGu8SwWb04sBOu/5hxrrmmOnXDA22b1EAXS4scbQkIv59KmGfLzbZayFM+Yk0NLE+41a+16rycBk+Gs7oh5mo0ew2xxxKHISNZCGQyTtBTGEebMS3lUD3XKlMspQintdLEDFsQv1SmDhWGhWDUmKEOWrFjT+zOvFPywfm+JWlJVMtO+L5SoLZ5Q9L75NktxzUo4tgRrYnXQxbXZ6RgW67A02EiPLLFnzNslGTlTDANsOSnXU0N8lIAFr5qneYzH6/zsF8nHA3gCyo16ooj8XMDeHcCzAZxW/FNF5LdJPhvABwB4E4A/EJFP2JXPDUvKNr1NWB/K1LiyIVbA1bT32hIpvcnF0t0rTeU5YpqOmXpOUwlY7B3aDMsBVpKKWE47YmjSXNRg5ALBhlypqce/mk7wLkU9M0RtmmiwdNVQhkf7NCMhcxLn19KwVD3JernqK1itJXXbo6BDvSSOpbIkzFcXt7Kg3UpJjXLGWDM6k0kiwJMb1kxMuQlFiYy2UaGAou0V1hhju1O+UNqbHohY24j13N47pn97bBliVg61IY3e0uqd+EiEb8FU+U6aQBDKsiwlwD5iCNiGXpwlqdcmY34spulbKZU6CpSEKjOuaeqKGccYsnOyXyTvBeBzADwCwIMAPBd5seh/BOCZIvKcSt6eCODJFXuiiLx837xuWGu7a/blMZi+fPfSa07rx7UDJdlKMw5JBoZRnnNPkxFHHlpsaVBMcw1zoxXOta0xasMKeab8yHStlpO4Jk74ixDor/kFIrXo9ky1fCnN2tj943onNb8xxgbTspRA/2CT98SwA/MrSCNRBVeveskiLtkhAQvlrecmPZz6vRhhdhePXTyWZ5glMGXKJRKiesd83RmzRycwL37x2oQZjneBfy97ZHR6ajVjmottl1SwNCQZt1KCE6oOI1IMWaeXzls6PSNLowkCiun6Yw1x7DC02Ek/sUBt+AhLen0QLwMmI+yYQP/zs18PB/AyEflTAK8neXeSt4b9b18L4J71+71Q9r9V+RqSbwHwDSLyPbsyumFJ2UisZzR4QeltG7+6ttG107WHM9YK+6StZZeOcfUEg8wbF4hUIHKRYyXvWINlPTU0no5fR21nkeg0ZVscSz0sZj1nHSW/tFG3sVBAe3+RcMXrMl78WQzzcqZiJj0nUSWfZZENbKSHFax4tciw4r9eU1FM9UIwf/ScAVBvmK9Bdpqxet/K5VLsOFJG4srNXpoyJcnAnhIEROrei8G+QG2KhO9Br2KiaXRY/z3+XtVj9dgtbhuTfSOQFyBkTjOeb3q+BZv1Gu2c+n1lQkIhcgFD1GuxYMyUPLXlDGkmAxzTZJ1RQfUOBgO56GyLw2RP+3Vfkq8Kv58hIs9ozrkPgNvD7zsA3BvAG+rvFwL4UZKfAeBWFBIHAE8SkdtI3hvAj5P8WRH59a3C3FSkbK/hxQOxrRXIfJhwcMbGwmU+E5RdA1YZEYtNMQ7oaVraA8zL4jMBGc5ty8HmWPlbU9RGnepXKESLoZ2wGvPbsbFCxNKCkIYR2XCWv0usu16LVi9cmaYTl8lVs1hrvPe+PEUY0lScu/Usj0F+do8Y9CDQuLTjhajLlJ9ZuBGTUfEnA3gsil36lyLyonPJeMpNLt4omBpWtBFM51ljCiQpdgx1CLFrdJru0h/W/Ncx92x5uqycpJ8AZZ3UkyWXI+qdhCHCBuPJScmvzjCNxkXjwEYEyrA2OB8hzVQXxZbufE/zJGPxnOUk/z5I9rJft4nIw3ac80a4JwwA7lGPqXw1gC8RkeeR/CQAXwHgs0TkNgAQkTeS/DGU+LJJyqLsE+x/CDpc9aJdubMVakzDGrwn82iOcw2vpKMVDa5cJ37RQrVJBmYwQtuA/mjMOG5jrHrxHCs/45IQWZcM64h17X3tuNncDVy6/EZ6TnokrT2WMF1fbJSfxqxhjBXbH86x8F0lXGKXOmNix44xZybnMKV8V0wGyccBuIeIPObMmU2ZUkVqO052iqgB/Y2XLJg0H3Zs0kOeeZ2EUslTzisNS+ZeUyVKEshTYwMIi/WKgKazhDgw1vXKehIE0y9kCuCSydPIC9ZhS/FmRa+aY4OlLgLm6aHBfeYl6++hkVWme4ycz5IYrwTwNJK3AHgAgDeHoUugXOHb6vffR/GigeQ9ReQOkm8D4EMBPGdXRjcsKRsSm6030wYR2uZIa/kEqhPf6FHP47OtbCk9NgvdVuJlr90wQSAORyZ3c4OV7BoyNsD6+oXZM2bAKiaxkWo16VXzC5HSljosp4Yj66mHkc5v2WDUIcQ8nKnpqK1AvGohrku3MNKhwRGm2r54a7+sBYOXy2yiRD2kWZQ5zdJJXTTWJWCa/qLDleLlNCwG+SdMt1sSmzhwlJC7ppTv4/oHdsdk/G0At5P8cQC/C+CzReRNxxV6yhQEmwKgtTNN/IDyFl2I1XiAEZrKCQbLZJgtih3RRD5qmmZPMmkZr5OmWPDQI6c/3C9TVRcnZK33LS5doYu5Wl5RD7T6tmuTRU8ZG+IFIJO0Zpsl2zOzmSRQMI3Xi2uTBd2Dn4Gd9msvEZHbSX4TgJeiGOrPJflQAI8VkacDeBqAbyb5VgC3APjMqvo9JN++Hvt2EXntrrxuWFK2e5ul8x/KNExQjIDug6SxC61CPdAtSbFDuPpj5cT2LxCI3hFYaCOxrcRtPJREeRuNsQkIf9mvB0bHGAgX4jnwdb0SjojlfPS3Bt+bHTODEeLAQv309xKMbmNza7BvWIB48TyKXgjyByppEtOL5SPEiJit8l9ZnmFJTxo9Geid4mjZ7qHu4/oHdsdkPBDAH4rIR5L8bABPAfBFhxd2yhR440P4q/EPgeiUL23H0CXHZK2YW8urfKESGvW0D2dfKnmqNiwQNrNpiRw6AYpp9nrFm4WAAK0eUl1dt9EznSUQwHAtmIlaMsQdFi6k9XobFsuINS+DjXf5TjnWw9aIiDwLwLOaw6+u2GuRZ2Oqzkcdms8NS8qGYouRoSdWUkFl92NoQMg80ZbMiD2ffZqQATZ47qKXrKmCB9EPFJUcOkl0xcXmGKPDyLBmDBo9mxYdIGtHS2h3PguJZJop6deqYifM1zXonURCFi5D6fzkNBn1Ttjkh1CW8XBkSXNLr7E5Qde3WWKDCeLIgw0zBize4+TlYpMmAV/uIt4aJWBxKYyIASXY/5p5yvaVXTEZbwTwgvr9BQC+/jwynXITSxf/6h0faQiG7jmJyLy0zaptCTSnfJE8q9FzKsOcobPXBvgbSTLSUdu6Lvhq5CkMJaoXb7CyP5Mekx7IOmuzHSp07xjbpStMz5fDiASp7It54qsKNMTMPGvgAFuMsAlQZlpWMubLYfRpHkWuzs9+XZicD4W8QrLlPTsWG7IpoAw3XauyZBsQSMtAz17izIYhEMkhsVMj1JFKNSZjTDUjqUhYJDp9qiCYF31lq8fQVmP+bnTz9eqJVSJ71eDtpYcmnqyJsc0YU16O+ar/usDrkghZ1FNMZ1/WhWErGcuYXrJI0AohO3b2ZbgQ48/+8koAjyR5C8l3RR+T8RIA6nF7GIBfPb7AU6a4EE5gzCkDwNt+eI7jrGeWTqPridkM3Wsy5WPkCjlNBFuzMMSIwc5XY9CubG9pLnAyxsb+Jb0c28VFO75qJ8PfZkiyI2tNYL7tW3myrAbmO1lcw9o0l/RZtzVNrN5BD8CG/To2zWsoN5Wn7LwJ2dair3unqY3zQL2WYCUsEhUGzLnLEDOSVr9b7601IIBhI5KTjAXys++YJVIxz8+2FAo1XCNWh2BYKYvHlrHBetLl10z2xCIxD4vFVnLlatELVolW0nPvWbb3FavlWMJQpmPSPWN7Cwmcw+KLe8RkPBvAt5B8MYA/A/BpZ850ys0t0X6Fg+7xojVgs0vhrNSoY2c2GTsko5KHJxsypDYmpUlAmM7t7Sm9bGaLa5oMaYZ6oMk7EiMoIeOyqocGS/rRSKLBuuD8iC1qpLKhTlg7JFHKYn8PfgbOx35dpFyt0p5RRNZfTrJz8/D+OLkSY1bd5hurXqwKwZU0pR236r+PRDAc5qQWcOU5Lyv0L51eMQRqKPrClPZLT5+NnsVzZOMF+EKynoe3bfc69WVZ62DlNLNQ09RlSzq9YP/0M1jKwvQGQ4v6PtDvCwkPyNe8mu2Sop03zCcpaDlMj2ITJkpeioWZmdTjR8pKuzhUdsRkvAWTiE05o4y2jAPcjpSPQDcat3PqR/TdP8BCQx4cL5iFSaUyVXgtTZ01aeV1MHmebG2wwFUMC+Vj1msnEriHDEO9NfJEhqHMzuPFNAOKa1gYXlXjzaX68VtCFlf2Rzh+qJyT/boouWFJ2dYwHjAgU+F7i3FDjyO92rBiCaJe0q/kZXearV8sG6C2vrbWmeaJ8NesRFvKqofGpd8JV/4itHw4wwj1Yj2nJVg+zbzFGNYR02TdMLXB/knPPN6tRy941QKmFyYG+7tO3hIp3OZEyDrM7GmIJwvLVsTzY8C+/l4CVmp2mn4X+6iEK2KRjI07FfsJ91nnZ8qUSyGp4xnMkwfsS2EzYWKftnvdKs5nHBaspFn/LjmfZJdspndTJksrllNtj5InJGJGIswEDYREjYgSHUa7Gc7Rocb4zql5xXi1rMdKkjw/H/LUeK8GI+24nitWaRgh0wVlC7YEvTDcanr1gnXLixxjxK6e/bphSdm12Gapx/J5OTah/mPPmmMxkD56pAZJhjTbUFFdgHVctm67pGo5BLIaCwGg23Q2d1xCEJg2ugouA9eSnprXA6KRoU2MGG6lpGkPsZQf0keLfGIGKRMoMgT6Y4Cl2Zwx+HcLE5wsEi5ZJmYRi9ssAaiYk7P4OClmzwH0WkuPUc3kcZ4yISBXLFB2ys0rvrH1eKSidEoFOKV7iWzB6GrT8jo+7sliHHqsOtFbFTuplZDE4Pvihart/8QnCCg5A1A7jEtaqgKmRywnHntloxWazghjxOgEyMgg05ZIHbaE7ZKsDmOsVgDqbetjxJYNTOsZZmXae6nJ45Dn4QrarxuWlI2E9urqX1B6u8evrhHKDKXTw/ltA2+S4giztD0/I3Hs9QwTWcU0z6EepMfCN0GLxTSDd5ARoxu6kXB7l4TkwbKPepY4aKNbWMzXihbITP7dYvGCKGkkwpInQywnEglWytDqC5Cnnk6z8KsNWRrJ8rTikGW/zZLHlh0n9c0zZcoVkH48QY+H741xGHW0vY316Xnc2MBeatslmzau35XcINs3qO2KtjSkieiTC3ptDFdTlvwJXreFzRZTeVhRkDE7XvMZYVqwNDzZV3KADQrcpGmE7WC5evbrpiJl12SbpUq66qPa42tpYpuUQHtQbZp7eABzMClQeo3FIzfCol5sPKxlMb3QRuy0djq4GZPiyYuGxoYvY34jA7c0xikmz5zfGGOHrS2ZA7inK2OyqmdbGzV7YmYsHG8xZgyrmC9p4ZgTrOgBO6GTL7v+lmb0nh0ucsXc/1OmqKSRguZY9KpFVFuWeY3UPtUGm/SSDatJ9L1JA0mszrDMhimQH9QkmrjaxFmWXE5Ww1KGR7MdTjZ/hGl6nfFbwVQ0nTQkGeofhitbvTRc2RlHT/MYuWr266YiZec7nKkej7q+y+idN9IztRHhqn91iG2QJtsHGj6BQcnTSBY2608krE/TiOFGbFkaBo2nEd0QqacJX9umwYrr3l3vKUn2Q5YZ4wY2IFaxraPFQ9B+Z5P2xBDSVQz9EjxImARMPV2nGUuXWgP663WVOKNT48rOGOivBnPKlCsoa9Zra8Fu3/FkAxvY2YQ1nCza0453BL2Wy0no3GYCqMQsDI0qhoAtjW69KrSyrGApvz2wZFAjVv7KTr0GGxGyY0jZFbRfNywpW2s0a7L9yhoyLv3f97/c55kJjWY4U2grTa1TM1M0GomuZ1gbcNc7TJi+tIM3TBtg6mW6JSlkLayErdeDOuNRDY3HwnmaXq9Qi/6e0Q1Y31FqsVqecKmW2h6TjWxsQbru1XjGdpz19sAIIMWOKabXQglTxhbGgGJNu5K8cJ18KY12uYs4fOkeM9bZmTh6+BJXbvHFKTevmP0zE6k2E8mApPhYifre+eti0uiYhVjoOYZVQiTZ5i0ngZRoseoQ4rJkLNlKW5zWy6Akx2LPNrGG6FQsu+T9byJx0HcLzNB2GBzLeYWP7rOpNQtkbVsvBeu2L4D95YrZrwulkCQfT/IVJH+S5AcO8CeTfCHJl5B89LUsi8ZfHY/FQGvZXy+qS4MFnK3ejnK2hNPiy7TXFMxLj0U9GFlLYoYoNMaQqubveinVRHpimmorx97pmKYagpxO1PEh1kyKzDZXTGNoUyxtzU9JF2Ne3BMzr1Qso3qrQtC+YXVhV54akbIFXw3T4UonY0XvFAtOjYTZ9kpVL5K3sw1fFiO/9rmZ5DLZryljMYs0ejRDzG20xxpT63YvtpVq2TqbqBajwZx7ONYuZRTIl806N7vm57aEK/KTuCl64mQYYdrRq4SMOTRGsc6rFsu7AxsTq6XMfGyOy4iQgTVgOmAIv0M39jDZtl+X0YZdmKeM5L0AfA6ARwB4EIDnIuwVRfJxAO4hIo85j/wuZPZl7H0JNof6+hWe2X9vu2aCNNRX2qyfsCQs0CTm/FYxC7b3eIQcy0X76+5ubZPrWJte2tqI61j+RCy796NRGn1OTpiIU8I28jtJMWJhFqUuTdFhPlzp+WXy1S6FEf8mvYSFFf6N2LFip2GB2Hq+/g0YbCiz6h279yUBWW5Yh/rectH2a8qRQpirQehhBiCAEz3gx3w5isw1ov1bEiZmw0AUD1iMgUXQO1ncRlfjoXZyGWxtZPZW9dTe1BmcmqbrwWyv6rWbjdt33RJJDc4Qcz37rjM60WNYTgbxH3reSROrEdOPy11kPVlOgBBqo/mJ3qQjnoerZr8u0lP2cAAvE5E/FZHXA7g7yVsD/rcB3JXkj5N8Lsl7nHsJBN6T6jBZQ2oHq0VrE7WNMTfSDBhbvT7JkKYr2ldxb5VjKQN/oMFcroilguWypGvUFZPpT9+BCb00ervThsuglzDkuraEqejJAEPCMkFCKEsfrxGNaLvGGFN5chxI+beJ/4peskqU0rqMYZgR1HPUu9ViIR3LV71j6hk7NcIGnMK2hwl6UD2cnmGbpWIo1z43kVx/+zVlXVo7VO2GUB0w2UYmO9zYBPd6SVIDNuyu/uYAi6dpR761xVrOSJY4IHuhoIqJJpJIUSUzhQEehuk1SgSsfNRnH0lWNLRl2DVidYkLs8uNTjLYTZpLtXZSrelRsWHb9usy2rCLJGX3AXB7+H0HgHuH3w8EcCoiH4myT95T1hIi+QSSryL5qj97yx17F+DaeM5258fw39759c/73uXMrmmuYF6arqGwnKkYuxpELLSptpytoQzYokanuTLuEYsGKGK9cYpYvmjs9Lr/IllLhQTi3pK5/A0BTJ+AMZMu84IBOc+EheFOAgjkLTj07S4kvTCUnuLLIDhTTNkaE956+G88OTf7BWQb9ibced5lvQklEAs2tnZhet/r6QDKRt2JFWW9LgAdQa+NaR9gSvI4yg9IhsBi0pJR1bKsBMcGvQHkmJUz1i/n59emerpGFSTzGmvBHpMsU9nDmm52Dlhiu9b2wgt6Xon6iXrHyJb9uoQ27CJJ2RsB3DP8vkc9FvEX1O8vAPD+awmJyDNE5GEi8rBbbr3n2mkjvS72KmPrer3LSMK/A6HHFAyxkacsKQzZTP7bpDkSMS9Mn5cPguVTvGjsitPX2s/p9Zh09UcmbwhGYYBh/Dt9B4ceMiNicbivsQWJWCW90EseHB/mF4Yg49V1TD2np4hkK5O1JmA/etOsn6q/T22YE5V4pYkApr/6eOwU4fSUVTk3+wVkG3YP3FTX8cySbKdxKR1dCCMXRAjjaF74FWM4F5rOEvVSzgB1IlOrV+3JCEPJcx0rw5JuAxmKSg9hYawDjCB1hkjJ0Air6fGkBt+3n8U3G3dPXdYbERsuS1mkdUh8lkKsVvIrK+43x0HIUm1PxA6UXfbrMtqwiyRlrwTwSJK3kHxXAG+u+92pvATAw+r3hwH41bNkNvQobd3TjRu+CtnLrgnir2B8qWuD9g5STNQNCc1KABCGodPa+6t6HP1lTFG9M0z5JoyqlysYe1ZWwzqDT48lAiWN3oLQU6UZl2VRw9TnN8bE221nJB1jdT25varDh9TOVyR7pbBpYk/8iOppucsxQkDxSQJGlsTJT5nUJLX+mVgtlXR5jFmPtSv+q6dugeQ8azD/CbWeAr9yAtTYMp0kQDlF83AeJjYEMfjcPHKh9mvKuvTDhvDGC1hjToHyfnY5v8Gc57gdAVDad7CVcQsitclml6txMBuJYEOSXtDUNPuCpPQSFwt6WnfvgNa6hfXHIlb+hrgzO6a/U0blmF7zuKaZvYuqwbetlJrV+M1IN8e0sqNhUGq93Xum1vIo2bJfl9CGXVgEnIjcTvKbALwU5Q3xuSQfCuCxIvJ0AM8G8C0kXwzgz3DGzYl3Bfqfy0KyMTBfALFlDVol2OLv4zTXno1B5L8RNvGHPArz97TsRvjd1y8YDDaHqsETCY1WSxN4VCKGoWELCqnS0tOZh+dns59cFfBYUT0vcsjeE+51iAG+rS1IO0JFA6T5hXJ1egO7UjAnPR4LpkH6moV7tXRIMRKqNNwYzJAPQQIlYF8sRszSY40tgw5lej6lrEcG+oM45eXrTV60XLT9mrIuW2uMmTR2MNmluGTj6LzG7GrYWcS0TfvaZH02hsVJV40eyPpyaMggK1lKqUabG0hRvC7NQt9Zjx0ZTcYuBt/HfCPZikbVC5ozi1gX46H6y8CAu57k8V80hT5Azs9+kXw8gCegXM4nisjPBezdUWyA9oA/VUR+m+S7AXgWgFsB/JCIfMWufC50WoKIPAulgFFeXbG34BobMm3Io9X09Zc9vCuoY+2DGPS6h3SQZvgZ472SQaCTp75IFRPY32gR1jDt6aR1y8rb3gyIXgOROFmpxIEh6S2WISOzClUkfJaoXn1gqVidYdlWsCqqdyzHRKj96I9FDPEY/NzkRUerB9fTfCup6Vf9V8z3sIyeLiVjJ3Z+xFp7lckXEdIckK+TtKK//nWvWxtbhvD7KLmEvcnrIdfbfk0pEjuZ9tt6fOWTSFvgAdp5ix1KLgBOgGDO3A4sbBcK9C+kc5VYnmpH424nbmcU8ziwNs2lep3UA6dkyvfSzMtTpOOV6NhirbrOWdpvcnED1GHBGC76t10Ow/ewjMSKnX6z5IXmPVhCQ0mYmAdLj+t1OYNX6xzsF3fMvgbwjwA8U0SeU8nbEwE8GcBXAfgXIvIyluVyniciv7SV18GkjORDAPwlAP8dwC+KyO3bGpdHrsU2S6LE52BskKb93OgJrpUlGqgBNuxdjoxXhINeX1RCLWHLp6KVSmW172t6IfWV6ivW4wKLLQufolKuWbtnesy77dBZOUd5cYAxY0a6jJRlYhUD8wvm371j3RCrOszpxxpC1pTfsdOj4xSExOkljLuYMkWJ2MiWRntn9ss+6qVCbJioJyM15g6L6ezA2vPsdzY2sWxcxpgSutgzzHXKxpSI57YkSNOsQ4SDni2bNKvFhtn0wSr7dk7rVUOTPjTt8hGvfFdGP35coP+e9uu+JF8Vfj9DRJ7RnGOzrwG8nuTdSd4aQhheC485vReA36/fHyoiL6vffwjAhwM4X1IG4PkAvhwlbuLTSd5PRD7+iHQuXLaGM4/HtvLbXRaPPDigLAxBmCWRbT2BnR/1stGK8RHBjlDd76GxBj2NA/OhS0+19VjZNaFi3gAFHv+lK/RHNzw3MR0mGLf9RMgazPavTFhDqmwjdDchSzxm+hUzz5kf0xuRFoNNm46LpRmHLsuVObWYsuj1IgSQgIUJAYad1UsGXMpg2EOE5PsB+ASU2ZM/squnOuWKSLR79N8FomEac6qN0t71ZjPovxHOC7GxyU4T3exD13PMDUogOyEOLJaZVW9E5grWx4Gp3e08Zwj1sfxy2h2xSoSMyTvmafd6tAISa16whDfXRIZ6JU1pCdkRpAzYy37dJiIP23HO2uzrN9TfLwTwoyQ/A2Wo8uH1+NLovNOuwhxDyv4LgO8UkWODVK6bnO82S9Ff3kLiD94u2fO0EnfG5gBzUQZp60bkYDkvbqYrkLQA7WgLJlLTjz1PjV3QYng/CgHzcqmBhM940nQQjWRsd5L1NrGxZ83sh5IYOzcE+3eXrvVyNTFfbDxgydvl2yW1eoWQlesIIKcJ37/S48CcYC1W53ycEHBp4scqiYsTEWAE7xg5Q4Dt5ZHvBPDZKPEeX0Dy50Xk665zmaYcKXErJbVjaqLVfiU7V21Ua2uwqFr0nHmMmE9uYiBIGjYiiFbP7WVJVNisU6iExjAtTSU3aocFkf5BSV6oYCJWaXHxQKDGsWwhv5MalarpxBMXtzhK/uyiLUsw6w3pShMWos1oCBkjIXPDHi2j2AuhGOlurbm95dzs167Z118N4EtE5HkkPwnAVwD4LORg3lZnKMeMavw5AC8m+ZkkH0Hy7Y9I45rLlrcJwDlsswR7iVa2M1hIdoA1ZRORJs1MvGxLJDvuD/DqwrUhv9FwZp9mxhiIVExcbDHcgZ42qeE4oHqVzLI5WdPNtFOeteTBG7WGjbJL5EmzS/ekweyve83iuakOek8bvKThxCo9G7YjQPCMmbfstOrF4UypbwT1dNWtkyIh46lvz6TbMuEU0MVi5RSUelykzsA8Qohzm07OjW2KKvZ6li2KXkLyQccVeCi/A+AVIvIyEfkMAH/zHNOecsESiVArAvHhzMFpUtsxSLcZxnlkaFDMPMdj9bd19Dty4SSDaehNu2zaMXTypHYo2lIbpVAjWTOmxnzZ9fC8bX01pXeBDMJW6A9FEqSFZM1Whw66x4gh+xtUb239sXb1frdyNnXetzuq1s3KclKxGLh3oOywXwfYsF2zrwngtvr99+FrGL6G5IfU748D8BO7MjrGU/ZhAN4TwPsA+AgA/xDA449I55rKtdtmaWkP9HqNh6hLs4k1y/nJMM1RTFg0PO3fJe6ajQZrpmdHY2Ob4wYF/W2b8UY7Ac0vx4/FFCwoXz1jBrpXLfYm/fsWlocB0nkLc/UC8cozKBvj3WA2aot+5qV6tspSGGEINHjEDEMkY+VuxmB+9WQtYY/LuM1SMFeFcA2C+dVDtoQlOiCeN452bGu02tmEuwNlgRIo+7QzZ9bLAwH8JMnn1N+v2jp5yuWW1lMG+F8bRhQB2ynvSjbs9OC1P0FYzT78NXvCNqny0UD6Jo8SMkHrpLoXq5yj2ywh2DT9YtjAuC1h2yMjTrpY60lMszGKgYwZrkYtliWElZAcL/iqhvJksMCslWlZx+hpClpsWcXk2BX9z8F+7TH7+mkAvpnkWwHcAuAzq+pTADyT5NughE68bldeB5OyOmz53+rn+YfqX0/xAbbeE6aNauwj4/qhwZZIqQvSYG0ZEiFrCxF6Km3vcCt+DClJNwqtnpKaEaa4kYhgULQ0bdpavGj2huzIzmvNnFc+ErBcuYy1+aVhQ9P3M4yk2D9OFHOaisXYspgfbE0ztmlDOsyrkIcnc3zYGLOhy+bj9Q1Dli0hCx63Y0SAXYGy+wTJArsDZQHg00h+NIAXo8xYOpcQCRF5f5IPBvC+9XNPkv8JwCIif+M88phycdJ5ysRtUTyrDicUgrFia4RI3v3cz206wgytORmnxlA1X1POgZy19q0lbl4WH1KNWHhRJFLn9rjWfYBF51+vB//dfMQuQ4+l6zTEYuGrlU3lWcP09+Hkag/7tX9a27OvX4u+kwkR+XUAH3FIPjtJGcn7o/Rw3wLgG0XkD+vx+wD4ayLyrYdkeD3lrLMve5JQZVWvJzoOrmPdkh2BaHXljJg13tZAFT3bxiOYHUjRi1gst0j2nlmjrPktti6OGhMadqK9SDIbJ5HSqyOH1zV769awxjBDO1iDmyTActLYVAAa32VB+/UQGTEEkqRVlJBfIGuEkaWFkvSUVGl9WhveYu3LATF/q4bYwrA2m9M2Ig/Yxr6u+8gO7X2CZIHdgbLPR/GeAcC3Avjk8PvMIiK/AeA3APzIeaU55TpL0wFtwUSpOMCqnYtRrvZHG3QMzMeAe1hBkAic7y9ZdBnSHW9gHu1n+CQm5t4zSXqKhcsSsbjoa/hYTFrKxjGNLUuXztLs7a+VIy4gq2UKZWkNmXrEZDDRwbAzLGtx1WJi96npdwH4YJQhh+8j+WEkX4RiTB9/Dct2jeRwj4H1UnpknFqM9er4QfCQtWmaY63PTELDH5WF+t8AXDvexXoNCuPubHQNsF9lv+rR1woblmWoV8qhpKvvfBHd2mQItrNv6/bZXApjaQ4Q0J0LCtbE7dVzShUGd18xdodrmtkTpia22LnoBdP7fVrKiRaTGjOG4AUbYBKxY6RMKV/7HCCbgbIicruI3CkidwL4bvjq+IeXmLw/yS8n+c9r51GP34fkpx+b7pRLKJWYyWhvV+thxWOO0RYRzB+BpDXGoq5iOd1KzYxwRcTJS58ma9kbohbLEtMccLW8f6USqvLdsO66wNcls/JrOem9TXia+l10T0wtRMxb49xCWSxerM2Poau5LCFurMFqzJkT1kNl235dxuV+9iFlDxKRx4jIZ6LMYHoBgJ8D8GAR+fBrWrozyNrDuPpq4phgRY9R34LX87E/w0QryRoWZlzI2LhX5yEQgUMw6XgpYzA7AyaOEQ0RjYSQbTsNeNWzuEwnF9GY+NXzinTtVnOkxmcMrnVNb2U3lRXMy5+2UrIihiHLeD3g3r40wz4SK+haYXkIFY3XrMUsyD8Z83BPKjGLw5gQxyCowfwDLJTtGBGWFbHXPgfIZqAsyXuGcx8N4JePKnCRG6wjOSVKP9kK5vFXm6LeosaEla8MtqSqmE2qJKhAcZCvWswQlF9eCU4YFsuvoJZewjxJt7MhYBXRPkSMTTkJ0vevjCEYMUZMA+iT12yJK2gH4zdYKJZBD01MsY1YrC0IWy5YHaZYGsz1pNOrGAPWW/i9ZJf9uoy7lewTU/a/9IuI/CLJ3xGRJ13DMl2I7D+UGX3kst+CsKoiKC9dKbNtWj1bVX9UFu0BioQp0bQYCZuSnZbJ8Hy9t7dSzrBHWxGmOlhvK7CWuEVT1DVipoTNCIxH0Jm3LqSneuueSAmEDE15+zgLt1g7MORlLbz66iHzOLNcljyLEgyeKQJxX8sip4C0+1YqSSoYibSWmO5tWbDBlkkQQE4rAQzxaPqRMttyMY9ZwI6U83D/7xEo+wUkHwPgrSiE7ClnyO5BIvJ/AADJ9wXwswD+HYBPFpE3bGpOufQyjMMNHIvtb7VdwqSiNsu2kLOkmP42vajyqXauzEgftQ/msjTlzPXJsARbaRi1Hk6UUsc4nhTta8qbw/yTXiNehz7Y30JZuhW76UtYpHtVfouVq7mPRPCKDdY0O5aYnYP9ukjZh5S9F8lvR1mx9hcB/Om1LdL5yCrRAeAsPylULLuY43JgAPdPU6SMkUOfrZhmLlsezsuBEkxj9wKLHSATZuuRoeTlvT3UF7yvBZ+CK1NZIuHyypdmkbHuuiQCqMaulFNnbfZt3ockVc9nXGbM1hpqsWCYvFPnWNMJa7xxSiR9uNLtlxMsEmErJXR/T9LwaB6SPKlDoD5cyVUMiiFvpaR3q+idhhgxKbxQPW2nPvvSCVkt18YyL7vkvHqTOwJl/ymAf3ouGd2gHckpjRQGk4hG7FBKS0xO/HvxttGSsC3WItEa2JZo8GMsbss9oNjieYDFOw/Cl60gw6uHtSzMYSNhRYnlhKbb1i/OvvSD5VOwkKZeh6Vg40Vfq1dtuCcmHWvvCQDznimhsvcqYbMvweQlk0YvevnMe3aEXEZv2JbsQ8oeB+AD6+eTADyE5O+h9D5/RkS+7BqW79ylPPjjF5Q2Cv+t39a9an6iNL9R3/lsTmX+HhszMiZJldbYh3qVmG1hS2s86r8C7+w0FeuMXqfdpBn1WjKKpn5s6gc0nrqIcqzHiGHQaUODNTrJcwb/DDE62dKxhEzw8vdYLk/3FDqc6YTMMVKge5v3hCxupeSeNdR1yJaYjxZH1yrDcSI4W5DtdZIr2ZGccrwQRGoAagZrw+1XMlL7BbTGJsZ2tfGvak9LILwebTrS2ik2ghKDH0ZlQUizbalWkLxMRu3eOXlamjQrGRvoGVHTNcRGemFdsmQ0R1hIszDIJj8tK5o1y4yM1d86lNliaUP0w+Qq2q+dpExEXg7g5fqb5K0APgBO1K6MbBGrTdK18jBo3MJ6mrvLkmK/1rDUvvUhdwMR9ZLrnX2a7uFyxmA9GE2j4kqq3JXeY9oozeww67l3yK9Jd05o02uELOmhtROVyA06bYYv8WCbrl4YsWttwffNDEtlPE6QIrFqyVP0grV6qSgBq8OcdD2bVZmGImuatoxGrUM7lKllO5On7GoZNdxgHckpG8Lmb/2RbKoSs4F9aZcyKwcBI2SBjKBJk4P/ACc2LTEBYFs3JR0thE1yqtadwW6u7ImZsUqaYgVXt26q5wKuw7b88Vo2RtMvbDak7azMeooELEbWiuq3xDFiVC9ad6f2kqtmv/ZZEuNfAfgBEXkFANTg3J+pnyslItGbMcLYHNt+Dgj17HSJ2YPUQ8HrNkqbHgeR9AA3NM4OgppjHcmL36NBCqOMOnTY5qjnLalAoWZhRDXWj6nh+l+1HWsywrycJT4PS4vVIUgji5qOWJoUNYihvmgC+kfXKt5EG9LUYP9gXuh5dZjml7BI2CoJs7xi/Fhz3giTBmt+O1k7TkQ2btgllBupIzmll3YhbV+gFaHBiQ3ztRxC7SsXhjYXhJWYdCEwxRDZ1nRqAkP6S9qGKKTNMiyZ3yliHftlWRpbo3+rZ0q0nl5Rsg49IhCpSMxOFiNdHaad9Pg7G8/+2AhLF21BP2YcyJXpRbjqNeRO7G+/vMahctXs1z7Dl68B8I9JfhuAl6CsJ/RjIvIn17JgZ5WhBysd6oIRVjAnJv6i51jNulhJs3zT+CxnQUErErVgbkJ+0SuV9ALGIdakKYAoYWgmAvispiamgdV81MCIFM9lRk4NFsfXHjq0GB35sX75npXfghjoD6vnAAPN2BoZC/Fj3iOWUBb/HS95XL2/JUlK4iJRU8/UKhY8YCmjiOmQpw5ZtkQrlD1hIb6sFMPjySwLI2aHi4A4xdWKybiROpJTeslhHn7cyBZhHqJIgNR4cEEzTBhsejZOIQwDTWezqnmIbxMfC7O/NvpWkzU7RYStlGIFA4GJnicJow5Whzg6EmzWkt8GyXcYiFhnFTrDvgPryFq8aF5HiefEi5zeMTBLV6xWiEmztA+Tq2i/dvr1ROQ7ROQTUFbCfh6AjwXwWpLfz7Jf3X2vdSGPka39Kwu+Lybp+6Ze182J0pOxNvVBdvYwtnFwvufaLmyUfG2UIwdKbUQj4QZmaY6h4fChphl2/3ASJX6suyYRQ2riZg9WvPZu59gVxYigO9gDKdJtl5pqRCwOY5qJqaTLbb6e4yvwp8oB6Fbnh8D2tRXf59KGM6XM1oTcWdYnk7IHJuxv0D9CTrGsfi6pvAalI/mrJP8DyY8jedfrXagp5yOi7Wlg3/yADEAX6x95z6s3CNA8pDck8RQN2EdjqigDIxNtUVwPLJ+3vsYYK7Y4zUrGrTLARJboWLPuEKNe2ktOP2sYmzxyGTW2TOjzx/0ClDRFA/oRvGmK1bz8mh4/+3LLfl1GG7b3Nkt1e5QfBvDDLE/LBwP46wCeBOAvXJPSnbPoQzxankJ/qTsZ6J+1RDzsT02TUa9NezCcp3+IHguFivFlWY9HYmv5Mdcvld7rwO64ttE+TY8ZC5nn7IbXLPZM7bflozbC48usPKanXifVqV41NhMatAxAwOhEaWB38rIWo4kA8eN7WLZDj4qhnmdDmMkDJl5MAZSsxYkAmq4SSY89do9Z+Ysj5XhjeL1ERL4DwHew7Df3GAAfD+BrSf48iqf/B0Xktq00plxeacMz6kF35MRz1EGWjAXDv1lvnG62oXpMvVxd21esIyv174LBkCtTmpaPpscF1JmXjGUvrT7HhIWK1TSHsyvD3pcdxn2xfumKESa1DtKm2RrZ4sbMGNTTdgyBunr26+BaknwIgL+DMsH4q0XkShAyYNt7tu1ZG8SNGdIQgAaPM3nKAc0Pg7NXMIlYU869sZJmCh5lOWadRuZgVYZE2skAoYI2DOp2wY1DS1RcLa7e37j9Gwwp71iWHkv7TcY0dfjQyBXSdy4CH24MGNa9bpZ9utg+ZBmxoR7hBDKVNV67HE+2dERMSVdZDgP14/kF7AgRAKeyrH4us4jIn4rID9eFr98TwFcDeG+UMIwpN4iMZtPrsThq0KLFJNIJEZ1gAVK5wTiYH4FzdMZhU6+Qk6gQMY2vohsISzNmpp4lqWVpJwk4VtJMJQnGmssyKGcgVl3lGiwYLiHKCv7V05UWhbUy+94k+b6U+LGSBjIWieCBsst+XUYbdkyJnl/1Hgbg6SSff75FunayOrx2VTA5EqtpZlwbGMBlZcukio2GLEu7ZDMBoBqZim2XZXS8j8twTLIXvfnENcZcJOtpCSsZ0hixdqkMx5o4sJomIFgWGZC1qreoh8zNT1ld/xRLiCGLsyshZdPwxV4XkXSpo31AyOQUi2TMBlLkTix1BubmWM4O6X18OdLtsgvJ9wPwpQD+MoBnX6WO5JTjhDG4u33LKRY5g2saWRvZU7FOVOpmWXptB6vXQzYaqRweqytAMkpk3E7J8zAv2qAtcqCnp2WPVVvWdQwjzAjYeDkMG5rsDGYctkwL+tg1cHLXX9N9Zct+XUYbtvfwZZD/AuA7ROT4IJXrJGvrkxVsTVraHiGO/8IJUjeMWJPiSpLD7Iw/jdPaicXejJYpYLTjUg2AGwlPS8xodFgocMY61hJIV9bv9IZYO3upJlvbc7eYoumJjmKEq6DDlf2dV3JWjIYG4UvGBIiB/Nb0KUiZMeRXr3GMI/OYNT2vdW+e2nBlXPJChzet+BL0dSkMdRIYdozwUvYmD5TvRNki7hRl54DXiMjXX+cyTTlS4uxLbacQHaes57Aupk2Uux4fYUqNy/ImZbZhUaywgtam5IlMAlTSUzqvUU+SndVFZpVtGP2oQ5mkz74MnCUMg5Z4ZqNfFQNpqwokb5xus2QLgEfDuGDh4vHRTe+WFeMAg5azYuZtVAxAzEq9cdJ13q0SwaA2yvS4Og/QOFSunv3au7T09RL+HIAXk/xMko8g+fbXpmhnky2vEbDfUKaeUkaEymOR9OLLUmSYptTjY72YNoJlUKBi+q6F2F9LcwvbqB9jdinbFb0trJ7gnUevYCwLG8yaWkfyWgyBZKHDog1Q1fJbiSrMkKnemiMvpxnMgATSFUgaACdIiQUpefLA/Fivcn+L90yxPCSpegKwBuxrfJrqdX8jIXPyZvkdIbEko88Vkd8B8AoReZmIfAaAv3W9CzTleGElNgBCP4beL6nnxE64QMJcl0gCKhr0WnH7NXjebdeMgd7pup5TkH5RVPWS+TDnQM+GOXMnNZYlDZuqhED/9ipED1jnCBxMAjBClsqZe8USDW1MNHrAiDw7kyhLYVRPmphHDQfLLvt1GW3YXp6yGtj/CwD+PIAPQ4nPeB8AHwHgH+ISbvK7tbclsE3aWoKQg9Cph9EucpM9Ult6Ia+ECdpWZmviIDc2AGlYcQvzjPpyWq/LOllZL7WVvvV7mu0K1kq0qL3ElKP9dc+ZeuLc0MRg/qZUBQtFiGQqL0QtKYHxVkoDvbjNEmQVM3IIQveyjHkr5sOYes3b2Zxx6NG3UmI51f7C4sNaj5kkTJfBsOUwjnVsC65cT1OF5FI9+g8E8JMkn1Ohn72OxZpyHtKYzO44BmRp8XapzEfRsme2Gwr1wLkdYk232DW127Z9UUU1XqtgI71ybKmbhhtKH7pULNtW98blVfg9Rsy3WfJrYUsbtXq1Z0qy7PeWDaN9uvxGnjOv4AAL3rRKsNIG5ageMMUDpnoSdQ+Vc7RfJB8P4AklVTxRRH4uYJ8P4K/Vnw8G8DwR+Sckn42yRuKbAPxBXcliU/YiZSIiJH+L5F3r+mT/rX6ev3+Vrr946HrvNfBmFY51DZ+9QntymxiRDUOnt57hkNSlN3tNO/KcVfLF5ljGGLDcu1rXM2wZYLUcfRxbbrc95sOkXW6NHkN99WucCaltXbGTgFHrrfteGiaWb4dBQl6ZZDkBi7Mr23P7SAa/XU60UlSGdv91KDPki4S1sWbhry6ZcZRcPfc/gNSRFJH3J/lglGV93hfAPUn+JwCLiPyN61nOKWcU1g5wY7UAgEJfzDqgUrGRnUVNq7X5DJiSKE+S1dT34RV+xsB+m+2ivytSuuW3pdl4qlrCF8EOS7CWF9mIGjkKWPuJFz6QqSEzrtkqmZKQd6ggtMvaka7gyCjYcbMvz8N+kbwXgM8B8AgADwLwXACPVFxEvgbA19RzfxjA9wX1J9YFrfeSQ2LKfhvAfyL5RBH59QP0Lo1sec+2PWuNe2vfNK0RH6q3lpuD0TVtebSYhIYrJcbCZ9/AHvpSlrBQH7Oeu8p7vSXN3vGyiIyD8iOW3FsN5jMvB1h7fWrHN00WCpcLMsCqMYfoUKZ4QpQGa2+BE7dRYcp1Ken4UKZqKiHLQflxWYs0zKleMMSA/RZTj1mqnhG5swT6x1JdJWk7kiLyGwB+A8CPXO+yTbkG0rXDAY5gW/XcugIDom2r34tnCo1NrDZXvWE1VfuuenSLGAvm649F5qV6/VIQgGPmaXM259ggiF49XZpfq4fFJwF0r6JmPbMhFm2pffdhRz3BLE+3flqDNder0ztC9rRf9yX5qvD7GSLyjOachwN4WV0a7PUk707y1rowtQnJdwTw50Tkp8PhryH5FgDfICLfs6swh5Cy2wC8K4BXknwzgFcBeJWIfPUBaVx3OSTYf+ghiycHgjLChoTM9EYFyO7vTlbSFPS9wYRhJT1zm496eKEX2bHEii3sIVtpf1zJ4ukaHN8L6+mqY+F38zlZ0DT3UJZan5GekjU/RmgUQl4sNl+XJZgCWqnK8OEigA5/Vg3bl9KWUmRMs8SbGccL+fVzhyJhq7FqYZmMY+XOK7ZNSZAr35GcskPUbg16soIa7B/E2k8N9l8xxDVsxBNt9XJ2BRWW8I/Y4Tb+Ucla3bMuESdhCFPJDXyMUbE6DNqARrRWjSJXhhuYsXhMz4tYsoyoi8E2op6wtHp4vcrU69brmfWsy2S0uofIHvbrNhF52I5z7gPg9vD7DgD3BvCG5rxPBPC94feTROQ2kvcG8OMkf3aXLTpk8dgv0u8k3xWXfB+5NVKzobAGNL+NcaGjSIHZtATJvGZEj0eCR0fsb0wzqTVkbIQ1BE+D/CPN0Or7BIDYIyunxi2YtOFrk/QVr4NR6MTjM6LhYbgu/dIbEWuuGlusuVNmX4i2r6TZxw5ypxeLKWKMiGi9ZwLIqaWztKRKBL7grBdSyViOJVMVnSDgMWM6RKkYwrEUzB8x0exy+ofKVds7LsgN0ZGcsi7sGtXYVqbHn8zmPnR2mOyQ20bnLYum3g0NRju9GlNcDAqIpebHbjFVQuNqs3ErMyLp6cW9MOv5pNavGVKw/Os7Jg5dRC9hGt4IekbIUqLxogWMSU+CBzB3V2kB/eU74XtemjWFviWO5GTnZb/eCOCe4fc96rFWPhnAp3jeZZFqEXkjyR9DiS/bJGVHDbaKyG+KyA+IyD8/RI9lW6ZXkPxJkkNCR/JLSf7qMeVqyng0njGpx8p3g3RczH5nH5z9Eh8mC8mlv0lT02nSA/N56XuNg2oXTEzxCeGvNu72UdUeZ8Rimza9RpFwRww5qmSwOchtS+oBthk2esk+VEXzhrcYfLkLw4It4YK6BlkmSkWvnUXpuO1RCYBNHRf66v2enA9NshlGdPN0mvevtDwqMZNMuhRb6nZKaZZl/Synp+Bp3WbpNHjLjhABr9TCi1FE5ItE5P8jIvcD8OEAvgPA2x2b3kXarynrsjXaYTaH7VExVAYnyEBHPdFr74qIsQEYu0Fdzy92ZINpqt9FJ5BVkhVNo8ZlqQcqZR6wIYFphg672mw5JjosGM+IJQOYMWE4wcoasaYyTbLHyC77dYANeyWAR5K8pTql3jwYunwIABGRXwnH7ln/vg2AD0WJxd+UY9YpO0p2BcrVc+4P4CHXrBDB2dN5cwKriK7nLMEL1DxMySPVNe6RnnR6A/604nXKabaxZYoVz5eutUUzFiX+oHj1ctxb1TPXneRUVU+xwIgspiKQM+NCtZzaY4nYYnoBqx/zqllJGgxrGCwDjeEqnxp8n6tXdaUOj8ZrXI6TviVSNhQl7aHXHm2wf0gTpyXNRHLVM1aGHW32Zb2ohbidptX5lcj5Uhi6iGy4MhQnZkfKVYwpa0VEfhPAbwL4gWP0L4X9mgKgsXfeuEvcl64GDeT3u35qR05tnO2DGx7xuASGoJ2RrnageKJ0NxPNzto6q16clV4SLfYy7PcWxyBAltjfMGKiZfEYMNezN0eI10qtlcVTlTxj6iOoF8SuWaxAvFgr8WpdT3uAx65rGcbU9KoXTMoxw2K+dmMUO64TeB72S0RuJ/lNAF5aq/O5JB8K4LEi8vR62qegdPyifA/LsmG3APh2EXntrrwujJRhv0C5fwbgK1E2Pr8m4qRjDRvJxiSAXYH+q3pY1xMJQZ8jrGnoOzC2DSej9Zw+O25glfGkTpkbCfgEgWEdAHO174GlYjdl2R/TeyH5BCVWnd1xR7tzPHeml2eiqR71Kpym6+Jw7aczfA8OfXS9bvWO9VheEi3P6DRvm2LmYTtcRM4vpowbU8rDOV8K4JNF5D3PJdPzk0thv6a4CKTMoqSSlgAqM6rYYMmv0IHMQO6kjvNWApUPwEhJb4crYYqddy9iKGc2RAzEJOvRy9Do2aQEzas1UlrGoWEPZGgfjA0W95+qeaQlLUKahDs12jGGiK28gPaS87RfIvIsAM9qDr864N3IoYh81KH5XCQp2wyUI/leAN5eRH5+lchUIfkEFOOOW+92/70LsJXudp7H6Q0D/Q8py2B+tWN9sVqsjWmInjX7Fs6zZdJS4xjrIeiP27CmrbFtPeY9qoEeGlKlfa2QF+NJYTgyEZSAmefIqiBWds8+eNYilsoY48RiXFj9zfa4JD31ZlnVwyzJvF1SLaP0mP899fRtCDPopfiz4+T09OxG7QbwNJ2b/arnmw2734Wa4RtHWo+Zk5seswasBAdKXAYJa4c5rkPZ8ZMYd5s/Hufr392mxIjdxqIOn5s6klG9fxYnbNfA9VJO+upgqHeob86vt7ZuTJl0ffjUryXIerwc8yHifL2AnJ8EfTAsEhutOJvfR8h52K+LlIsMCtkVKPdUAF+2T0Ii8gwReZiIPOyWW++58/ygt+olkzaGCzveY5L+DLFtvf4kGZCwpLcGiaxigOv1sRH+e1RXbQ8jPW2nvZ6TsEENVzs9BOo+lL1WwTjAdIiwzE4cXXcnPmv5aTre9CPpajHi1EckYnkI+BBkb5YYMN8OOROw4sMNmDSYZB20MWURO3WyNrikB8i5rYZtniYReT2Au5O8tTlHPU2XUc7NfgHZht0DJ+dSwJtNLC7LJvY15GGJGAInqJ0xnWDZip670m7KiMrpgHigzkgXJykxvYUQOUV03EjQ005iYHDVSC3V/ruiRMzqjvxJMyybyg0xPWXxgoVyygamejHmLZKtld5tIHJrRmoL20e27ddlXNH/IknZrkC5dwfwjSRfAOABJL/+LJntmn3ZEY34wCesvpqlwcpbOd3SqGfIUC9rbm3jEfVaz9suvUTWkiFIK21lvfrf6PKpXh+PV/8RZPc79LrVfdt0hX66XiEqCJN3Qi9QvVEjrCV4oVMVbGDb/wvLSgTvlVViPJQZhxFTx7OWMXu2YoYtsYrlUzJ1p5kHS8eGJO+0GDNLp2IIG417WordOcCOW9FfUHqaax/UNX7C5wkrSa15mur1dE/TUQW99nKh9mvKbikeJP8dg/kJWENWL7m21zScWbGByTdiZqTACET9oR0stTtp5eqQRsXccyVOxoKeefPrDyqxajBRO9hhgSDFQFepelqxDsvEKl0Xw4h2mrsgeM6WBaNRQntfRGOdzis/+pjrYKX1mh/Bn3bZr8voRbswv/muQDkR+ct6LslfFZHPOWN+Y2LmeeyJxXRqnFT5iuTe1jgwO9P10no5rcdLYFtjDCAPMpWVNFmPp56LWLCotbZA6nw40XM0w1X1VG1R5VqWqOeXyfWGQoYFWttrG7dgaghUM+yZsBSulu+12hUjiwt6LKYZbFVnL2pCiw2BwohVwpRQMZIwx1qvGCA1mN8sf9ERQLdL2sJqElbPhBmpq6TwjOuU7Rg22GeNH2A/T9NBs7kvUi7afk3ZLQIBT+lBn8ZnKslpRx4CKWrdVZmk5b8ee9W3Ax9OdFurbXVZGDYU1/MrvoQ0nRGBEnTIUgd3zfvWSMsAY9lMPO/fVvNZNO5sGWC6PRNDvWv+dZslCXFiPgFA48fCPiMtlrqrUb9iRr7q3y3sGFaGnfbr0smFBjPsCpQL512TIF8lJcdss1R+rzCEePIgsW29vnya1qqeZMz1BvkFHVvba5Do0AOmxzsyVa+j2rwhGct1kVhOPSPVoSVkOACjFaYjVYb1aayl6UAlNV31qjexHTZWg6+6q9Kszp9UZG8sfpd2yY1QFLFzDxcR4M7z6U2+EsDTSN4C4AFY9zQB1dN02YjN9bZfU7J4LGyxa0MP/qkAJx67VQ1d+Xoq4Al7g6JpxsVlpcGiQYmYpblZ8C4/1AkLoy1EWMs8HPxRrK13ymekGO1z80l6ZmHtmITvmnYxN60RdeIpqCEtAzsr6S8HxzJ2iJyj/bowuakiTM+yzdIxeluzL3WmZPIGxUa/Q29oEKRue8QQnB+9W2kT2EA6g2ctBsjqmbZVR9tmI9Z1MUNZOjomYSPy/qotC9v1FE2WpRmWNIy2lmKSGlCl8Wp5mFGx4uFKDCjoeRk8iL94yABdMqOdmWl5BU+ZB+ZnMwjTOw3prGNaxPJXgocsfDpsdK33kzM42UIa09M05XwlLcjd9twQbFyHhbEFagetCb5XzrK4DbWOnHriBgbMPFxx9nzTcdNA/Y7yxCE+Vwj1YFOVgA2WrdCOsG1obja8yW9okDiI/whDt92K3cFaMS/M47yWiAvC2rtLK5GW9ajWzis4uDb7yXnYr4uUm4qUHTqcqTdzuGWQesFGTCBgLR1xF/vSIllvlGY1BD3FWekpBtTWAxskuqaHqjfCuIGtp1mkn4Kux7FK1swjPkwQySufy1m2ZxrXHeBaFLwAXBpPGAjU2Kx+hxbCtlJq0ixOykrWEMYczeNWt1IKwfotxgbT9cd8cdm2KD5seRZPGXBxU8rDedPTNGUv2dpfuMgapp71kWVYt6fKIzZzZDxzV2mcWPXpBmKFtqyO9Zqs5Ug9yoSJkbQ1/f7jhGwdkw6r79FRBenpSSpnGANIbPg4uWrbxN2wpGzcoNZfTCOE+tDYGSHN2OMYxC7EoVI2eqxjSwKa+zljA29ZxCC2Pk+Bsns+e+gY9Lwpt5jVIwSrmrfNhs6Cngan1pS9N+mYaK7Ry9aQrtiG9XsuoXWeGg5kfakUVlGgipHgor1p8XpVbGnWLlMvWFksNnCn6O2KK/6n4Uv1ZPXDl7pOmMbn+Qr+FWMlZFpMyRh1CDXwMfeISbivNX9dzf8s7jGthfDKuf+n3DyyxJ5aeNzLnpH0zpoN6aHaGo1lrXbY7JD4Bt8QiKhHjW6/lmCjqxEUimPaTgG3iwttOFTXYixkBFhOiq0VAgjrO3odqlWJaz9WzIY7ReDBdZGQKebvGMROfwrPIMp2J9krJVr/dLzYzhazN5vWB6XeiXQRhtmisAzELpRF84hVOESuov263PukXEPZZ5ulNAokGSs/dqQZRsTy3pHxr2NGAgZpDnt0SgoGz1yLRarge1w2woA1JxANhh4Dxp2afWaXdh06La1zxIwriWl1GNMceMh0CQkqafGbpDNF6T8CFoYokwNNoDMh0yzLgFmaKb+AdTyuYiLoi1nix3Q2aHa8CUQylrI9UtqR0XaUdMqU6yEk3a7Wzmh1VjtP06YmCFg0UrVlnnqaORO1J6GjG18BNf+Fi9toorFD2a3OhI1s4q6ywAmRV98VY3hLNEhKgBpwTLpiVUeGVrNTL1c8f4z5STXfQD6tfkDdA7PM5tSu5lGMLJTlKtmwG9ZTdpbZl3ELn/bc1DDXZl9K7TkFz9YSumzJdxY8UqUHlT1ryTuGEPMV9MoB/9thildsaeIPzBMVvFx+MfS8AcaANfktIU1bCkNrrr1OhmUy6nUzpxrDta6Nk6F+yyK5d2ueOrFhTr/GSuA0Dqw6/cPemIt5x9ST5uVYELaDwmklih4vli+LkjP3jpnpE9ehNFjUk7IFk5bd1iLTfTL1YugMyxVM0wR0EdnDRXD13P9TbnAhkBcmDN734Bmzd4D2pBZa06DG9GoKi5K8aIOqrT0psxq1aZltCrZNatxmst0nFTNa4eX0WFw9mU2aHvMGwuPCluhVqlxJDXggVYLqRBtgpsicZpNo+WgQb7z4Mc203R37/CxZ1nL7UCXVE8kwfEm1wFLIWwgdGgWh7JKraL9uLk9Z02gyNvICKVka0GnjYtatWcXaWIBSlD5NtmkmrQ29lTql441TLWJ9dsdh0ci0jkHGH821jqVv665kzXq70QCHs+w2GGmNiWrdtRSSwFi2nHYTi0U/b3MofNj90nK3z5k+I06e0lCkdocbUuVxZO5Vay5UKaVhx8tV6mVOuYEl9bTqIRLWGhvDUJpbT5a0HYt462vFnMsjDxFGti/bvz5J9mmyP5kN5mEpPSWhWqcNbOvdpZ3PdLwlbiPFBuMmpmRsdD0raVszJsqgZX1dzX1kesouseznOetb/rEet44sped2R5rmzWkaqbmV+qLuLmcMDvVEffZk8sXVjksMMx1g4WO1jljQKnkpNqpAjfVqZ17S9QYzxut1aj1Piol5u9S7pUxPbZB62GwGJt0+adrm4aKmEzxq9JrQPLSj1i4Wn5fJE4phbfmf/rXhZvemtdhQgidtXJ7dInL1ppRPuVHFbZ/ZB/jvTtS11UzKsY4eUEbzgnctjYpELz7UOtbvJ+uB8jarvrOpsHg1M+vMWJruHctycgL1pjEanT2x9FFwGWFVzLOWyyJ20UKwRvDc6ZplPuSYsWDM67ZK9dI3a5rlxWOXXO4D5Crar5vKU7a6zRKbvxFSJt8llv4MsfHxdUX1BKXMg65TomApstOnya7NJA6pDhQYkgkMQbawrh4y7vSENj/sgClJ41hf84118mZamnIsZ2zCBRvpObFJmASsK0j0bPXX13p3ydlevGDrD0vjBVNPWQ2EYTw16Il6z8SvgxO+U3v52MSAI+Uq9TKn3BiSbK4RK22Y5YG3rYxGPTXVI9vmVaBqj0aj+mWYcaxXhvuY22LsOFas6yAv8K2Uck03MM1v6RtbfR+sYQDr9kwrNmpZVt6FzCv+R81SwSFWK7/y6lvRk21MCdlZzcz0lF0SGXqNVttu7oFF8rKdVuML04CEiNTemkeShVgBU/OYqARK1QuuGCNmdfblkKiFIrrHqh6RagSSlyuUv8VCGTcxLUXwmlmMnF4r9teSNQjKSVuYbVMvnsWnaX7qdrdOF91g+y1oOobF6BlRiSEWhXmXnAn42mKhnNLMyvS7V4Yc243MazmXuPaYFVNJm+6Hae+ZsjemwAmZEjzVk0LGdAanE7ZqxUR8GQ3vhx7Rx3S587hwtClTjpZ1myu5AYZOZqeRbJSre1oVPZVmFfyQhgTjU21NtO3RS+6Gp7ZRnjR6sTBhTa7oVZNsY5O3SuBGrc68ZLQC+2C2Yr5VoV6nYFCba6j5+4zKHtMLwZBmuDApP/eGFUwCFu8BIza8wfvJVbNfN6ynbGt2ZYsnZ9MO5hxn+yRN2YoyQrAaUT9kyFEfat1lI9qVSAbE/0p9inXmkDR5M+ateVAPMa2ooG3LsGDYWgwI7V9tV+PhSbUINjY0cdMj+uvCgGldlPS47fLmH4f8ymVxAhMLKInYttQr14UBWLvvtDso9pFwk7QoEXcy1c7KlHRaWhZD48ns9wp2hIjs3PtyypRzl01rumqwQ2OsRsLNkuQmZDbSk9F9f232vf53Gt8VmgKqvQwGKGXOolcNU9Cq+e3AQkrpRzS+0py0D9Yz04CNXjSOWamUaOl1CsRYWqza1FzH5gWSSuPxY/GaHGtpdtmvy2jDblhSNpIUFbUyljby5mRc9TrAsJG+rm3Dln2pnvQYj8TAOoO0em8skDL02uzBFx0ulFB2JTBxKJFjrBqWsi9mrxdnXzaXK2EOu/duHWNebFqA2C8ssy+buA3tRFZMGY8TyLoKv9W3mUWp+dkQqsaYBUyPB9dXu6VeObfclwWwe1RIWCRmgNB/l88pgNOyAK29ZHRCgHvejNDpfT9tzf5hcirrnylTroWYX4n+m1ItuI1GVIlsIdiZZBIXAicI8WWBpLCxT5WwaV5c2GChI5gwt/EkwZO4TZNZJ99rMmG1MLZHZaiDhkwQvsm6Hag5mnFr32vR8DnmTjOGWDYEg1g9akEvWNg+zSFW62h1b9KOwx3wNct8CRGPTdNlMo6RLft1iA0j+XiSryD5kyQ/sME+n+RL6uf1JP9NPf5uJF9Udb54n3xuKlK2vTYZsM7Ht/S272oKzNcsqlEY5saNXuIOrPxpDAyDgUP+7jZMOkyHEtv0/St7SPqTx/xWEiYNxl1YTEXPkxU99pgB9PvXFZOefosRTcfzIMwJXezZF1d9LkuKbWuHK5UUao+/9RgEjxxDPoeKoKxFu/aZMuWaSn1uR3Y2el5acwX7HT1ZLTb4Hn+vvQ4U29LreqHxsydm5KvHdOQjxdUxpyXD/NTiR0K0VkdGlVWst53Nu6G1uyNs7bpA67F1M9Zll/3a14aRvBeAzwHwKACfAuDrUz4iXyMijxKRRwF4HYDvq9BXAfgXIvKhAB5N8r135XXDxpSNZNsDNj5e4qK29HZglRQ10Wfrelv57SqLETLCKJawdF7AXldQ98HtMY0HQ0vyihpsHmccq2xa2XpR3QPW27Z8zZI5aYwOo8rixzrbZplUshK+W8xbPKf+tcVnQyG17ssg7kzPKDNBNS9xxerJKkssia30X7ZDOi1LKQUMqOoWQybu/TIHmi+jYVsuJUwJ2dk8ZVOmXBfRd3+KUUoQxswA5ZE/oW9bR/gIQ0m08cQHPc1gzYYxxIIxnFiNT7RDyYYt+2DB+mnyIUbMvYjuIbMZ+zFuZIiFisXZkGjqED1neg7HmCRjHllWdAKW/MqMS6+1AOYh81mbrhex9ZuxLedkvx4O4GUi8qcAXk/y7iRvFZG3xJNIviOAPyciP10PPVREXla//xCADwfwS1sZ3VSkbOhlsuD88Q1fJRY6dDeCRLJbG+vnRnx1A/M9sHYfykhEqG5fKUNi3qwLRsJirWJwqA77RYKWsTL86YuulvPycKVPYrD2zFIWLGKNvcc8u2gH9LqrbUkYYhEl2RupZKhkFz11xUO1iC97AfH1e4peXIoiezg5wOzaanE0aN+KFp5DCXp0r55iIqfuzo5eNilbKRkxlLDHpSju9+BYEQHuvPNo9SlTjhKbGFXbu8XQIrZbhHZTj0QvTVzYFZHYVZu0aH9F7Z4qwmxbMSMNtrC2TQSC53oay8ZAeoiAcRemdVY7t7jtbriTGswYm2sXydLMYyUA68QGyYnZZ3GTERIVLQuyarjg6TplQqf5hbJFklkrIAGzBWUjdqDsab/uS/JV4fczROQZzTn3AXB7+H0HgHsDeENz3icC+N7wO45G3gHgnXYV5oYlZVseJaC+/AIh0AdGygZkq/pplej0xGpD9Fgv1OdQamD5MI5N9TDyVvmw4ipm5Ki2QcBn8A2xWIe2xakx7K9fjDvT1e11hX7HQsxeujoVq6wotlFNo1nCJmON3Ugco71N1TlFBl5Xr0WcHNF6+0MVVrCSsM7QjDWTSOjsQwhOIThFjPVSsggjalFHLFXBKRb0W0FB7x+aRWbjvY2LyYrUbufZiNmUKRcpbUxZlGqpzYZ1jyelEARCqZ01h9J+ly5dj7tvbK21N2aMqqexpSEWrMGKMWIiWrswoO4CQ8bs7H2x6Ar/4YrptdEV+ofXR/MbSfVKWXJ6GgFZCN3VIBWIrqeHoi3NLLJ5Kyj5qpiEOvgK/21hDpc97NdtIvKwHee8EcA9w+971GOtfDLK8KZKHCBd00lyw8aU7RXrBcA7YH7j9xquDCSlxbo1dro0xbNj0Bu8OPfGmmfYMaxisUTexnLdkw2iG4/2moFKrAQthECsMkgjaWNpSsMWRa5/qG+63o1dGIRfuG2xCxHqAUneew3wV6/baPcp/bvEWoglV9NZvw9pgoDkvBe4N62LOxs+94Lhgkx7iEiZUr72mTLlWojZu0EHTMmTntC/sgk17Ik7LKrXAvAhzpRK/W+AWQteQmC6eZcqxqof+lRtmrntK6ZpRgtYC7r4Jt0tcTIipqB4qu1SGFbOzvDl+kUsTvpKWLC95ukKBMzJl11s2AK0DVbGMxZA2gVo+7u8j+yyXwfYsFcCeCTJW0i+K4A3D4YuHwJARORXwuHXkPyQ+v1xAH5iV0Y3rKdsJD5oF0mTN+7yc88b3z3DDF8DKM3vnF1iPW3vkPtgyNRF0u/ck2ox/dfJQYMxZdjpaY5MjTYOeWKMKcmJxBFmSlPzi5dpHatDkCNM/4pXIWFhFmYvNU5rxJqUlDe31n+GoUWoaRTTY9ICaGuS6ezKJjMB0nBleJoV67xhFn8mHXSInM6gsikXLJ2nTOqx1pSKdsq6CFT3cllcREjb2mHUG7z4t9wWHaZe6T4Zw4bcolq2FmPG8mvEbWdL7grQDltmbL0nrFZ2G+stgqiFW9ErpR7Xr/yQDoPV4wg+ZnIe9ktEbif5TQBeilLozyX5UACPFZGn19M+BcB3NKpPAfBMkm8D4EdE5HW78rqpSNnWJuXbAf3rD+p2moJuE29LUTryZj2ONs0tLJwj6OO5zDhBsNReFhuCJSJYThxTwqTDkobBQ011CMHShHe6NO8SrxbT9OLacbiuYWGbpTicCQAc7QyiQ7kL4Bt+a361x7zU4HzbhFwMK5ubRwyWYdPpC9dVVjEliPBk7K9hIdDf1/SpG5FHMgUdntTV+zOGiuWhTEkZj3cf2F/mLMsp103WTK8oISk/Y/9RT0iRYqlDJpng2eoMkhOidqb6ApDA0iEN8YO3Og7TVGuaDGObk/+J9rutcNybjk063dBAzK/f0868cEuI66qscCcG1OUr2jT1bwjarzfQvWCxLDUte9fkCQKHyHnZLxF5FoBnNYdfHfB/PtD5dQAfcUg+NxUpKzLwJgxIw+CUPhl9wNmeL3DSsaI3eMDUjMQ4qr2xRLaa7LhGOKvvbAVbS9MCZdv6sTadlbo7kRobubV7QOzGlkCkEgZ0w4u1Cuv3XJQ09kQmx7m1uPoiRwTo1MoUgxykpulOt4ARYXV+5HQJ844R4bFKKcc8jhN1/0+Zct1EUEe0As0KJCuFlDDq1ElOkYOERJm29WiI05LzSSMTOuQ24D9xWBXJVocJV3osFHsZeoNKHktc0yzWZNWuW2FGB1cxsxij6+kF3cRGFtGwFb4JDrDNOuwvV9F+3bAxZeszFftj1oxWnAlraVkzlvXzzaHRKI+bixuCtix7YdGJEooZDVFW9h5UXMm6xdT747+dxErEoBRBv+c0dQgzlCbkH6Qp5hhzChPNIFos2JB4TUr1wv2zEzxov+2vmclpFnVl892vefZyuX5rzaQjgEQhZHExW2kwCMKOALFI+iCEfTPPIGkUtPkcItxefPETSb6c5E+Q/EGS73AORZ9yRWUUl1v6LmFXjEBKpDk1EZZmZJ/ggCQ4yYkjCbbyPioWe3hmYNhhyd40nduYpu17GV3uwePFGAeWDICTw2KIW9tdCWEx0kjjEdHDZV56PZRtU7pEqXebMZ0ZGUcuvKy0r7XHW20+EReYzdaUoSxECjk6ULbs12WcxHTDkrJdErdL8vea1Bu1fqe6bZbEAMN8cx3/m9PMaXR64n/3wUIh7G9bh2is8tYg+aSOmNW/DHmzwWK51BNGrmExRktSmq3naj+sY6i1fpHotAwvkqtQFZFUq2R0bChxjDVzh+xcgfioSCoOK1b1GvYuYdJOXsIDkBS0n+svXWzZ2YYvRYA775TVz77CHYsvAnieiDxSRP4KgJ8D8KlHFXjKDSE7Y3tXHun+kDc+XSWmmIBCBFC/2z4dsXFXGyZSqeDAc95hoY2rjdaYt1YvsaxGz8qdzWzXslO9Y1gDE4K8wPSgLCtxpyJ+HUzsunRJVdPWkMTEBwvW+f6N7wmiJXWYGBZwh+yyX4fYsIuSG5aUDYlVaAc5Ziv2hkYGwRW76dL0BzHN6BR3tRPs9TzFrhdleoMeVsKWQANSC+n1Yk8jutgLhgGGphtBWwpDMSconl9PalsMYyxmF3qCgS/V8pVP3OGjNTWlI8asl4hNxtSIpWsG18szLCWlOZp9iZCKro3mRE09cXX9scTW6vfGQxaJP9X7pRdM4veIVVW95mcIdj2nXqYtvigirwdwd5K3eh7yp+HctwPw2qMLPOXGkshfpLbtaiOi9bIJRNBmFTuRyJMPwwOc4nuVTAVqYDGzEvTo2Kpe8IBlDHXZCvUSeSeuOI9avQIIgx6VpIRGuIRtkaQ6GNTy0dNsGy/r1kYpNli/aVkYkVqLxQy1p6UEk4ufa8VUIhei8ULxy9fF8w7bM2ldjpHpKbvk0hGu+sC1Q2sDzf4nYeSkzaMEyje9naiHZlYfnNwMl79oMY6xUZyUNow+aFUbTTsLKRRWq9BdGvoDHcvCjLHDnLQlDIp1SfrvlTSN1g3r7id2dTCjPhhOJoq3qiVcRPXAnYa8PS8drkzDnxL6hXUNMTPO8bmzlfsVixbrtLEgMf3TlaUwFBvMzNxTBDs9Zfcl+arwecJKUmuLL5qQ/AySvwDgwzBJ2RSV+qCPYlxjJ3UZBbTXvuqa563rMKcM0S8CHs5gu8REAMveliOAA4yrmNmCYZoeUN9hNvw5qINeG+3dtmlquhbLxqhY9qBcTgJW36FWnkFZvDBdWYw6r2C+zdLhpGyX/bqMnrKbKtB/yyW+Bons0tsDi04sxUYkby3NBlM8zSAKeqXMTsZAP9cnDGAVa6mGL4qo+TFhoyFLYECCQpppspCzi4LF46mKefNvJ1zl0+0MsoKlBWAJ2zLJFoDVixPPTXUJ2Kh2YqbNiVQgm3bcMKleMNfzOUdYxRAwJKx8JHncjhQR3LntZdtn4UVgj8UXReSZKNPHvxDAFwD4wsMKO+WGk9juGjuaZy3Gk5kPDe1QtZVx8sBB5dq2+4QTvswTm9/Qdu2G0CZ0paqNSF7Wy1D14rWrcifTzpCBp4lRmizXypwYGHTzojGn6tQ0SYs7UweYjbWsYEbysNR0jrhPu+3XpZObylPWDuPVg5s6xRM8OCe/a7t8ciJ76m2AbblbQzJqawms9ciETwI2JphafzaV0Pbeexgl6I3LRK5MU2aZ8diGI5RyCpZlfN10ycE4gptpJXAahqhj1RctccLEsF6qKZEB105DIr3e+OY7iSKkexw99kT6RHdgRH1umni1Q6Xu6DT8HCCbiy+SvGs49w4Af3x0gadceelsIXsbaAN7caakNlx733PQNlawZKN2YPFFH3qH5DJ+pZBhdmJjpCrWGj4Ldh9gTrSKlZLWwGYD3VSk4o3hyx6ycG66sEuw+ZlWYnitIzawinuQrbME+QPb9uvINbWvqdywnrL1GZMq5iZKjyXAjrx0RMaUGmpUXVS+joyr5HXJsqeqnDBwo6seqkcqlVQspeIZivFesMDT1nXtM3+IdlHFUu9aPV3vjI5plMCy5B5gHMd0j1lTEWSPmrdRqWnCcFjb1jpkUhfXYFxUZ0B4bAmeWm4jywz7bOqBFgulL+ueFU+a2Xs7R+BLU7QTAQQ69St79pQo+f6V1MyVmIVNyDMm8OFKjUOhEcKyzZJfBAbsGBHBubj491h88QtIfmQ9/Y0A/t6ZM51yZaVd+kK/Z7tH6zlpe1MrydxIc8cym72OSJg5IUrztd4Zoav3F9OVY3xjzK0g5JFW76+xZdGDFWOzYsdSDZy+b3QIRN9bcSRGJAfauuEbV5CAziI3q7YyvJgumhYFkvIg23GDXM44Jz3eVKKfBWvpUC13NWHb3G0o52W/LlIulJSRfDyAJ6A8JU8UkZ8L2BcC+P8CeCvK7KvPkV17JW3I1qKuBffGpy/5dHyX3gizdpU9S+3vTg879FjrM4pvyO3Y01Oj1blzygmpJ9jghcD09dRj8dpKi7V1EIENqgmqJ0wNWfkubK5r00PtPVJ+rVvQG7R4fhWx0ztDZXOvYEOLYW+U4oGrniyvTbp8fjXaT+VgECNSejHyrMzozWrXNGs+VnTxdFm4IS2CtcaRiTRpHy5naIZtOquLL4rIlwH4snPJ6BrJRdqvm13Mjozsl50kYDLeUV/bwgp+CuAk2n3xxWBrnt6+BWXtCm8LkWItlbW18aka45vqo3oiwd6pvQk/lZCk+lcLRngBA2dLQb6jJm8kVRkOM2blClg1mEpCfXV9GpaIVKgDQt3Ty0akxKUN9FCvdY7UZirqoXLVmuGFDV/uMSX++0Xkg0XkQwHcH8Cjz70M1o8KvYyGeZi3Zi2N+FC2ZIZt2n58Lb999aKnx3qLAct6sQ7r+YUj0D5JnrUZ2jlymmW6d2lWbDHUJliAjTRXMGodcg10PbSIVcDOq1m6XiU8VrrOwGk8WcCil9QwS93q5/0/x7w8YrnE6zLCrJxSZmUmc8l6PHjGRuVE8qpBGXIhfpJN3CGiPc2rEiR7reQy2K+bSTqbBoIyiLYKfCaOUHQWrh29ABJxGYZuINjSxpyqbR6la7FkDRY41Mo7hp29jHUx719XP5pun+TIYFaVBWF2pSfhthu9noIN1ljMQNYiafOZoAxKhgXPWF8VLfBhsst+XUYbdpExZbumxMdNPN+C0uMcCskn6GyvP3vLHXsXYHv9MWBEYCq6+tMeMrYGY0d+h+rZA5wJWSpLrUNcSoJreqkvErGG6CVSMqjFuAWZXpwQ1WJr3HcdY7Ix1rCDIfYqsMPiNktmyaXRM4ZEx4KXyohPwBiOU8meEaJ6ngQspJmGIRs9D+Kvs24HTjPHRs9owM7QWxRZ/9xEcm72C8g27E2489qU+EYQbabDh019MKFXltWGmJ8hyUb0niXkEA82WLXdcemGFitEsiGZne1WsgKfOBXyK1hZ7iK9KyJBi7Ojah6efEvIOP4eKikAhEveMqnVa49JjzFgbvmqtZQ9MKAuKdcOc+4vW/brMtqwiyRlO6fEAwDJDwfwAGzspi4izxCRh4nIw2659Z57F+DYWZTrZA3jHti++R2ptzq9ew0joOvQrOoNhkYLkekpVdKLGDO2Jms9QWAUzxXLMiZ4SvLi0+ymBR2W9WTFYJ+CYQizxZcVTOpaYWlJJP1X7sQigqWxBCWpU3RFsZfFYBalYXduLIexge0pIoI77zxd/dxEcm72C8g27B44Oc9y3lhi/GPU+IOxaEOh4mPftv2I7bCnw/I0HGyIr2U5sPvGZSxitf62Trba2pX8VpbtKFgTkJvBlXKuYENTItp/dQIXWK4YVg5HYtWSrJh8t9vCkbLLfl1GG3aRMWU7p8STfH8AXwXg465FPMbaLA7rCO177+sDZkOhMfLc0mT6m/PTHk7fA9upZw4eSYYqDQPquamcAl252jxiDEH5InU/2OiNCl21GNwQemKngkpQStnMgBCaeSgjfAIB2vLCvFXtb/ru4jngM5ST6mVSm0f3xlEV6TlS8wABnFogv3q8FsXoZso/bnRcSrdLA+tj7BktmL8WRuPJqjcMhknWlZBm8KqVvHS9M32W1JPmGIblPFwuo4v/Osh1t183k6SYsgXuKUawS5Ta2UQTkF9shO0n2WDewEtbPZWyZGnESidV274PqekkJwAef1U5DBcvW7bPdaKW2gaJBCsMdcopRBYjZ0Jde40QOQVkCR79ar9ByGkbbxyx07KGWbsGEbV+wdDWshT7WvJL7ybbhNxV7Xu8LvAV++vmnXCPYvV86ftoiRZVLZ9jXWzZEXLV7NdFesp2TYl/T5Qg4E8UkdvOmtm256txh0v5R+Okdm6zpC/UaihE/2uCQPX8uCWSEQTFoh7X9bbKglBmNV6OaSPKaSuJCBcgpdNnto7ZNQukRypByHXQpqfn1NmCq2nC9NI5DdZAA6w0/IKNPFwZS49O7eqlpSmCXq0J/KFo6xr1Ii44tU35VvRqnn5vQzklkzT917dMadI9g12Kz2L7uYnkQu3XzS6pw9o8ZnmKjYQWJTpHplPLpM6PAgD1ec452ylq20a+KjnVTpXm2mLZ6pWvAqlrAuXXVLU1pz7RRyIGlLWEjBCJcypIgwGps35aO2rJk1VJr04KYraXtRINli5M+aZB+6FfaGaHXnvb/SbeiGieZIyhxQ6ULft1GW3YhZEyEbkdgE6J/y4An0fyoSS/oJ7ytSg90eeQfAnJv3rG/DZx9wJ5Y9QHcZ+hN4/Rcm9MwkR7WPW/0Luy3Fq9QM7sM1yNeVyWArd1kF4vpikOyqju2uFRPXYQgNqDBNyzFECfKu7XRbUX8xr2kj39OQ4tF1Oa69Kk2JWF8D6YWxDFjNWFoLA0IykQqWIDCVscKRelYPRhiXTN2gsV6171RpS19MAdY/ww55eu0REyA/2LXLT9mlKl7wM5ORq9VNMbTVtn/S+N5IXZ+WTnAYt5xuWIDFMTodig08ol2r0aKoFq106WpiwwgsWTxWwuwvui1G9xtmamqyqfnIShzMbuLyfFLsReqxmNwZ51CrLqhbI4tvgFt065ViISu9ZOhG2WQjmL32Cx63keAV+77NdltGEXuiTGjinxH3vtC1BeWmkYs3FFj/lY7H6Ur6G51WeRAXayZENWlt2G3sgtHo7HtN2gBCPVpBPJCRvMGWGvl1/oVX8ZYDWNJS7e2JRlYZ+W1n4JRUFzVh8moZbI7xE7LJC2rpslTX5Fx0YfUjErJi0xzHnlMkgDjyZSREx76SHlaoiiHmMSLVbTk1F9UzHPZnjkiq2Ifa3kutuvm1m0M9fZCpoHqA0H0bCNsnwOjS9ELNp8J1A60z1nVpaf0XUsUyHMnnck0bC4zFC2DLps4bAzCVgkQuepI73td0aUlvYI88x7rF61XJZ6LgV1DczG+qpnaw1r8/aXmzvBBli9HcN3y75yXvaLG0viVPzJAB6Lwqv+pYi8iOSzAXwAgDcB+AMR+YRd+dywi8eO5JoE+m9qHaln3iM3NFtp7SvJc9eWYiP5OEOUlk4sK4cJMMUwxNOIDbXca6T/Vqw9ZqQHxUB58H4gP3aee8aQsKYMOjQAgDZrM+tbfgiLxkpOPy2b2AydJhNksWUlDepYQKtnw5Pof+/CjhANlJ0y5brLLhOd+3zZmxU7ldHsLWyWhXDmtjmpKqbZ9tmjXotVPVYDRwR7r2WJlUnYSt2XJbwzmmwtlqy5RkAzYzMTxTLbM1yrqquYLOyWOvPyjq+ZTgIY6ekMT3P85URX09wl52W/6EviPALAgwA8F8AjA/44APcQkccM1J8oIi/fN6+5zVIbV7Wilw/A0jGWn2BJf0dpjXJr3eaj/HqlBotfm8LFB74tRa5izqvL24b0+rK0pC2p6Ydt+uu5xdKbW3ugyPaA+J+umGGmwGhvyLhJ+Dg7moeqSTnk1qIrGAHd+HyEIfWCG721YH7CutfrT9v+Iqey+pky5ZpLfcyH9lTbx+BRlKUGwCumnT0AYMEGji3Dsmca3pWl6jHrQSf6IFnA2IFd9aSNMK16u81SQNcwAiXWa60zxoEeAza8oCyEbJyik6qm/KJesO49yWKLB+EW1p1eK8sBsmW/DrBhm0viAPjbAO5K8sdJPpfkPQL2NSRfRvL/3CejG5aUrU5nHhzrX9p7pBUV9blZI1VAMAxNhkFvtEyGL5ja68XQ1DicmYPZmVz3JWh1A8sVt/NKOUStVnNNqicpde7onALt4q09RoQZj+FSJoNWa9W2o9je21uQwucsv5ISIWnqtW5r1D4TYgnr9fbb6KQnFrj2CP3ByOfor7r5GuMt0RIGL5eTxJpungWxicX4vWNkxpRNua7SPLqpW109UtYtSY3fbVQX7qBeLutYSmy2HgM7IEppMlaz/yUXn63YlSV4nfKkqFCWAjp5bMNp2li30bCkfRbPorEfaZiitSMxzdbupHIipZenOqxcM+o7y3+3qwbEm832/XOE7BlTdl/WNQPr5wmDpHYtifNAAKci8pEok4KeUo8/SUQeDuDjAXwRyXffVeYblpTtCvSPAZtS/wLSeZeGegRsR+qgk7QseUkEKmaY9OjndJ62NUyf7xavbU3b5xYW6EwgZgpqJST+6fR6rKlfhw1IVMeCjJOEaxZOsWJmouP1q8OYoZgEas8MbkKkmuSYRXom/LdSrSjhKgUlyZi05VejH+rD+FeNdk1LZ3XG627kq3ycHCIdb+/T4XK1Zi5NuTGkDajP3xu7FE9qz1H7I2I2o4izPbYv/cR1aCMsYm3Q8xiNosQ6xBboRCbUL9nEBoN3UkOio6zcznQVMCYYfq+nua7XQ5tOiPzGsy9KPNVURjurKxqMLBazpT1Atu1Xvda3SV0zsH6eMUho15I4bwTwgvr9BQDev1S1zMQWkTcC+DGU+LJNuWFJ2VBCC0kB74CT98TemweYsDgCplXxCtitPxb1OqzRk4qJn+Pr12xjENhUcMsyeJZaTpCX3uj1SmvRB1bCtaHprHkTRdy1b14xumesJBdivUJvUG1WCu+o1yjaMNv6yCqR71SpAr0sMsAQyllPsPg4RvvZYImWFcKU7HzUq1smKQN2Pd2knKHglXzZ5uZaNvrNU6xenNRTbjAz/kDxyB1LygRXauHFKTeGRO9Kh4WGJskGI7SnukRDWEPS9toWpE6FxPwEZVhLhy4pG5iPNFhCVgZWz5kTrZIP0vsARg687C1mIylWiWD3zYYFLBWm2lhbebvprHFdr401YzT8HYasZzMsmfqUugVTvUXV9tYxBX1fRAw+3jB8GHbJDvt1gA3bXBIHwEsAPKx+fxiAXy2Xgvesf98GwIcC+G+7MrqpAv0BJSv9C6p4VzZueju2HRjBaNixQutpinRLTGi6NmuzK8MOTAQLlw6n5jfAsInVGUMSA2Wjmi7eOMZEiEVnTMOrqtc62J70vSz8ONazRSGR5weJErWmKObVG2B6PQWj3klZsLEQSmnVIDjt07NEm/0rJRiXivmFyvnl6IpgPFu9o7HDRAQ4feskX1Ouk2gjFwxsUDUIdcZfG1JRFsbSzk9D3MKPpMZ8QrKJrW0Z2tJKkUbGZqjXdNNTOeOErLYO3unq9EJ+oX/WlIMdsUp4d012COMXpsM+ClDIo1HMeovsNgZLW7BaR9ojcLCcl/0SkdtJ6pI4AuBzST4UwGNF5OkAng3gW0i+GMCfAfi0qvo9JN8ewC0Avl1EXrsrr5uKlG3Pvhwfl4DFeYtdmpEhGLhZmKyX2lvX+vfHAnFTPgJqLyuTOidDLcZQHTqxGVRwNzaqOnP4QyS4Iz2rg/T1NwzJw2ZeI4R87Jzo/etLXkpRSNeCuLelO9Y1Hb11mYDVPKrzq3i7nJhZjzMO0YhYj9LC/tRiqedM04tY0PNPwOJ1OkoEp3OYcsr1EmunHJgZnw3e21q1MbSXeqtZ9Ea2q2a8j/1OUgyqrk+WbBK2yWEhLysTjwDvpbbvihGm9s/y66xUrkM6PHgh1UPB9zaqubLDVJa0+4rE/SvFl8poCBkQiFy4euNlxnfJ+dmvHUvivAVOxKLORx2az01FynKApR1cZ2QbXin9O2rSq96sWoaR1ynpDRLdhS0cuKT0AOtihF1Vi6drjDmZ6evj25uM09wiZMNCVoyw7tPIfgzZWrA7g2Qda+69yMZtd6xPUomVZhCxsl8m63AEgw4gcQ/0pigSPHXJNPlQyQDzmBql0ftgh4kAOJ3DlFMuWFp7N15vvxgf7YNkMxR6a+02SyUhtxmtsJjMmETG6gQAhtLUdMiy7RFOmvmXZCFPUu1xl+cKxhpgXzG2OssSPE25cqLDFJ055Up+lfiMDXtTzhUi21o39Ybtg8VKKJZGqA4nZVfRft2wMWW7Zl/GQMv2pdet6Iz6YgvGQXsFlg17vZJikybb3CqmHpUosiemvcgGk+jm0wwDxhFW/+tGAsRL3F5bxbRnGo9HzKtQhkVPFUvF1K2EYIY2FVMCJo2eZD2XYmxOvQrd5uYyxOzOW1wemzRLrMgpyv6ZyPcBBZO692VrXMpkjREW0tzAjOwF49VhQe8oEeD0VFY/U6ZcCzF7x3TQ7JMGL0g4ze1vNQzBNkPtYYXdPqtNybbbuzRNMD+bmNLYNBdCKLa3o/nRgxErKZ56WcyIaY/tNCTr3iSG/KxVL83rO2BGnlDrjmABOoPqljDHsDb16/T8GjbdTDslGn6bxATpMTSYXrtU8CPszQ77dRlt2A3rKbNV8VckBW/SjYBIWKG+4tFt0i8YOMD091CvKKYhO/VWIXukzAO2C6tumdQ3U29VIINMmAfPx2K38RDZQ6XXaETOGAiYD0+WC5X9QHE4MVlHINvSBNV0m3J5WZplN5rbZOnm25B7urZjSGF8Zn4F6mxsaJVfM2OJFgRcD0HjKIrm0uitzSrSa6OmPRdTDZY0RsuJZIcdbXvkyvU0p1x92RoZCItglAlXnZmvbUqCEUgdOJR+y4klk8NQTuE9NsXUqMR2pDaDnibrECSFuSPqjAgQNktzZRvpm3sTlFDQ1jt3egqerGCAV5ihLGqTfVw1XT+vXiSUyCdlA26YRL36HpFGVc+x988alpIeFHRvuXr264b1lI1kFBPmLTV4HYJGxPKbeB0zgqc9uvQ8+wMszRPb9g4TOdoHQ9PjiwoCIwatxMVUrXCGNQfCeWuYiO9/m8/1NFvblvNrSU+to8gK5omMmq1A0F8WKjLIMBqZ5r5rfYhC3JJhavT6Ug6umbFX04598W49uIFeSHgP7DARAeT0dPVziJB8PMlXkPxJkh/YYF9I8pUV+7+4Oetmyo0unadMiU7b+t2Q9hi8wzqK5bJ1xnq13o5G+6V6uYcFMNj7GKtQv4g2lzAL1MtZwaUvJ2zz8tD61UadntZkmuYi3kFk1/5l1Sashzo0vb+9sXpG84qJ3wXrWC7q4SZhl/061IZdhNxUpGxrXaWWIJ0Hpp6SNcVxT2qHQdiB2VIZSG2+9DwDlgmo+PYf6uWKvdGklzNfw3Qx2fEo8jpWdOE9yYZ1cRFwqV6pxgCWNOtxIz/ie1im/IIXjIJFt1JKHjIP5E9lr9hidVE9zaMNT43ptGn6DRQzovB1eutN9HWbJHyCXveBldPGdI+U81gSg75NyaMAfAqAr29O+X4R+WAR+VAA9wfw6KMLPOXGkc3HNi4ki8YMS37sW85itmPNRtW/Q9tWbN4Sln2wT22vw9mXNa/F3AOBqVQDpaMBXeerMWBebwZPXiqk/x2VJaYdD0UyHLdhQkOIQ5mY6g4IaZ9cYAJY+jKJWsWl16tq5TiOknNaEuPC5IYdvhzJkFhoL2tZBo1TX/ZLbCWd3rDBAza8mNWKMViGW0uUF+2y9MtM7MLG07C1DmsYav0O1duFAUB/PS0+tHWzW4puB6xsFTUMa9ja3BwCHCwLYURtrNcOd3ZJWqnbWihHDGSLAE8jFjTIgon2Uvs0vac7wGIeay8wDlT3FBE5ryUxbJsSAK8neXeSt+paPyLyK+HctwB463lkOuUGEKk2atjA1XIMpPbGtt7lY1TT3GYBaZa7JxgQTyLNegd6TMspOTFZLeNmzWunn6jxOJv1aOuUSFuTn//tXnqVPO3AVuyp6w6wHfdhS87Rfl2Y3LCkbLh21ujNpOPtCKTBQX8e2hbAdYwberlXI4gzTiKxamPi9sW0nl0vDOo8yrFpXhRpGltIU/rGGheSxSYWLleoejsnQkmXYto3cwJYazbEyt8QWgv1Ti31WnQraNcTiQWC0xULRyPRdiHUn85IiUKFNB0JAwFdoLCSMADiQfmglrN660THgEfd/eYCSotJgx3vKTuVTaN2X5KvCr+fsbIi9to2JW+IJ5H8cAAPAPATRxV2yg0hyYYt3iktbbbaF9ZOahoO9K9LnCEuOXVq51bKeoqRL3BpbK3aHw5GBtzgZKxp80tazzHYFLJ2tEMlzFjW+OZlkCbgdRjkh2Vxj1OKPdO0LYC2XKNaGTLoySnIk+SVU5IrcgoETJaA1YC9hIF1E5xq52Ka0AJkTNgug3EcOdthvy6d3LCkbJc4sVFi5C++hCU3AzMhEqzoKWyPeqNXZ9XU9hcxQQheFe9F7YUZMWnLEurBBmvsy5reZpoBs+G7eB3U8IVLKiE/qXrak9LTpBoMauESB5ISfD9wD4n2rJsyFlIXhhpzzQ1TupVojg4390WBzVZV668vj3qPep9oNPgjIQSnXT6pLILqQZMhdtYhy5jejp7mbSLysK0TquzapgQk3x/AVwH4OJl7ON3UkrxDTYcpe6icVJl9BMwOmUGBQE5rgL2RALgtlUAD2/xEF5juW2TG0GGukRM12zZo4K5Hf8WozYydS3uPBPvjiaTEzdaYMfLy6Cz+EszUlxOixApWE82j2O3BdKVqS4WLLxKbUiiY1sXfKhUbRlYdbhKuoqfsho0p22XTsxcIiK/abqmIESaxVWfMPSFuOmLPy7SUhES9VMgDMGur9b9EntawWIXa0xno1cxWsRTH1vbmYjmlaVYN1nQgk8FKnq4mwD7doqCXamkxbnqiLVYCIGNms2KyEQs5+9H2Y1enVqZ6vxjizOwhcBVWU6x66SnTZ6dmYZ3LQNAYs1de1l34w2THvnH7yuY2JSTfE2Vhxk/U/eKmTEkS2ggAI1OGxWf/NNtaAGlJLo8po9mhTAyiLVLv0Mi+KdYs5aB2MWE5Ta9DrFeMCdalI0KGcWNzzclfJBXo2yXT9kxNotbzHbRnxZScWT7RQKt9aTE22y/5FyomXpeEhQsj+YSDZct+Xca+383lKaselERwVoYEK7iObehxQ29tCDLpeZ/tMOzQOpQEk8ct6W1gOmy5b/0SyVKvWoOpvWhHB0xOUb1jhVQlTIJt8eInLJYlErO2dlIx64HuiXmako2RGUFRy+4aOjwq2puNvd1aV/FVj7xW3jsWW88sFkUvwBl7iQLc+dY7z5YGgD22KflaFE/ac+pz83QR+aEzZzzlxhDRDk4+TLC2qX5Y0cIWulmbKKvLV6LR2bctDHCssxz7YIOyoJIDjmy729L+faGzx9dsez0+fHdVmzTSG2Cq4QyywTjGUpIjLBRlxW0YDPuorDvknOzXRcpNRcq2g9239I7DNp+h0FNqnCtjT9YB2CgAtcVCt7PxgDWGLfWiWpJV9rakxVqMsAEhCucCsS3X0gXb656isjBj58SEG4WBE93aepwpqS45xUa3yTc9l/Fn9d66R8yLkg0YAd8uqX4vL4BM2OIFss15xc+HV8UvSPCO6RDoWaT01s/H/S/b25R87LlkMuXGEq581wNqlroYAe+VDeZU+VlLtkVssbAuZTpvyUaKwUbuwlbN7bKkJTEYT7LNzcsn2/ZBBZXMmBFW1ZQhjHwNDXVITo2WnR+wWM6R3kqa3f3qwLNar/O1XxclN+zw5UhEwrpUfnBLI/zbJbaKra0HpmVIEhqo6bXq+2KWJPMJ4VvXg5OMNgmHczKm1ZCdWJOdoPYMQ3mtLRMLJY22edMsUQZJL+RcsD5D9TB1SFu9TmsdWwGQGHYXeH+afg1zG0xPEglpjgzvahlX6n2oSNnQd+0zZcq1kM7ecWRXxYlG9yiKdSqHJl47nIMV3S2UYaBXsuPQG2ahIZvYuDAkuzWzpBam6I0xDDCvxAI5ba9a9Gad9ibFiNvqRfPvA6x7HwQP2cgGe1lWZDW/PWWH/bqMNuyG9ZQNvWJjl0g4XF6AbSB86rEkd+oKFvLylanFGxFDbv5PytNL1Cx30WIMMV0pz/JaXoLbm7E8EDDNCIoNMceJlUTHWI6zUAwB8+vjw5QMSQogwTsmTNkJdJkMH3ZOWCy52jy9VJofwzmixjoPFTomoUOpV6zBNPFE3WJQLwHWGZ2xsLWQPuxwWtYUi3pyavkxeMhiWiKnaUHIcj/LLM4xdjw1ExHceefVcv9PufpipKbrQ0rAmey3zpQ0YxCTaOKoUptr98akowlzwwBra7ravqmENtphVtC2sv7n9NSWsHA7E+qX0gpvn5Qmk17xmjNcs/pXKik9iZqdVWyKqh3BuCWCn1/se8DMjkZrWq1awLR+St56bnecDbuK9uuG9ZTtCuCT8LKUdLw9MX+X0fFBOut6Az+JcbrcO7RZMVsYekyzIVB6Sg2s3pfT6DnUQM76s2BNST0B992wweCzitRt73punxjSTI0wgFZuXTViUJZU90Tocr6hdOGc3KPrT++/eVHcyCu3rLQoY3oNanaCXBV/YUQ9tdLNPSC63z6WGfXiSZIv+BFylVbDnnJjyOrIQHOWtVvnSk6OYhKC5FDydihmX60V1vZotql6m7wDGuxCh2EFy/lK9NClqtKwUm4FlSSqXu5EI57bQQSkya996dVj0hVU0ndTVWxQCbdp6WQE69thTJgVxqrALr/95aqt6H/DespGkoLlYwBUPKd6ZVSjSWBVL2LdEGHE1C2uDcfc0w1Wz90Xk9NSJ6F71qSWRUkYk0eufLcZSjUOItZ9pMcWq70b95D5cIH+TjObEgZ0nrWEYUOvwcLv5OWqy50YbVGMbvjtUPBGOiZ+3+yIxmrV7U80cynmJRoem32JYvDavS/1fjmmMWulJ1tuVTR4zZIsySCLPyeWll7Y4wwaRHB6xXqaU66+tJ4y84AxnyVAiMmMRoHW1yntm3n2ZTU2yS47CBHUUQZ0exqXDm/p6WSMG5i2/4rGJSZq+9TyK6bc0rz0pMWcMaRrna4Yy2YjASvvq1L5aBTtOotoWmz09B4MMPF8fIZmLGi9PtVLx5CcnpgwBNIMXc3/CB/SFbRfN6ynbCRb3rM+NursGCQ+Yr2icYcogRB0sieW8rR22mNN807fg8moB2IqGUvLWnRYSmWM9Z1GjHgHhhjTCa0NGekNilIXmfWPsSyyuUJ1aHjwLKlJ6eMWeyzYzvo7YqHnKHpk7dn1BWeHGGQM7SkC4PRUVj9TplxT0f7I0Hbrs22NyMX2nhxgrXTYgFAMzj12QtbIEWDnxOHK+HcFs6TTvnTlqJ2zT8zW8BTHerh5V3AFS5VoJGCZbzdkFhvv0R2yy35dRht2U5GySzf7coUkrBO57YczxZ4FHaNjDSbh3zGGLrYs25YGY4thQy/+9lzbDpgTk75z5jRxfSHGgq3NltSFZEcNUxdv7TFdLHZ8+waLvhr5PPU9KfX8DcxPOl3fQknUazfQ3NLbV0Rw+tY7Vz9TplxTMQcMMWxYjH+DBM/NWltVvO+9BSK3pnjkS6Hb1xHmzx50ABGq2BC58G9c2LU7Z99ydnaiJ0ZDTFVjbzqqJsLZqDKf43mEbx12oOywX4fYMJKPJ/kKkj9J8gMH+JNJvpDkS0g+uh57N5IvqjpfvE8+N9Xw5dDbYMH5a26UFWxDz8MqB5hImRY9yGK8Jth+2GLbZsCHMOsBhu1GfC2c2qwrsfIV6zWPoudVrW7k+m/C9Gg9L3rOsp7bAbt84bsvthiwgR4iJnVYIsbHKZOT2oEUDjCvbzc/qQ5d2HWPGHz17oTV/A0TSfdYRLDUIeVRrERc3y3eBfXWgQBO23XVmouYawHE/I4dvkQhjFOmXKT4BClYO/TOWbBRlbWItiMjOL5MT4chdGDrUGWy1VKGHkd6IMrw4kiPUpa12AuLSVZ7KpLsRjXXZShxhKGkqWuctcRHvPI9jar5aZ3idc8YN/Ry/YaYGSyuYjYZSXrSatdQZEho95HzsF8k7wXgcwA8AsCDADwXwCMD/jgA9xCRxzSqXwXgX4jIyyphe56I/NJmXpdxRdtDhOQfAPiN612OFbkvgKuwOvks5/nKZS3ng0XkfocokHwBSn3W5DYR+eizFevmlktswy7rc9zKLOf5ymUu50E2bA/7BQB3BfAn4Xe3fy/JjwLwMSLyufX3awA8XHclIfmtKHv7fgCA3wXw2SLyJpK/JCLvXc/5xwD+WES+easwV95TduhL5iKF5Kv23Bfwusos5/nKVSnnPjIJ17WXy2rDrspzPMt5vnJVyrmPnKP9ug8K6VK5A8C9Abyh/n4ggD8UkY8k+dkAngLgi5BDxO4A8E67MrqpYsqmTJkyZcqUKVMOlDeibAOnco96LOIvqN9fAOD96/fTDZ2hTFI2ZcqUKVOmTJmyLq8E8EiSt5B8VwBv1qHLKi8BoN7FhwH41fr9NSQ/pH5/HICf2JXRlR++vOTyjN2nXAqZ5TxfuSrlnDJlS67KczzLeb5yVcp5YSIit5P8JgAvRZmi8LkkHwrgsSLydADPBvAtJF8M4M8AfFpVfQqAZ5J8GwA/IiKv25XXlQ/0nzJlypQpU6ZMuRFkDl9OmTJlypQpU6ZcApmkbMqUKVOmTJky5RLIJGVTpkyZMmXKlCmXQCYpmzJlypQpU6ZMuQQySdk5yNaeWBV7fd0P6yUkH3Qdy/mjJP+A5JcMsLuS/A6SL6t/73o9yljLslXOS3E9Sf7Fer9/ou5t9u4Nfm+SP1iv5//FrY1Xp0y5znIVbNi0X+cr04ZdTpmk7IwS9sR6FIBPAfD1g9OeKSKPqp/fucjyNfIZAL5gBXs8gF8SkQ8D8Mv19/WSrXICl+N6vgHAR4vIXwHwrwF8aYN/IYDvqdfz7QB81AWXb8qUveQK2bBpv85Xpg27hDJJ2dnl4QBeJiJ/KiKvB3B3krc253wayZeT/DLqTt7XQUTktzfgDwfwg/X7/1N/XxfZUU7gElxPEfk9Efmj+vMtAN7anHJprueUKTvkStiwab/OV6YNu5wySdnZZW1PLJXnA3gflAf6wQA++cJKdpjEetyBXIfLJJfqepJ8OwBPA/D0Bro3ynUELvf1nDLlRrBh034dKdOGXS6ZpOzssrknlojcLiJ3isidAL4bvhXDZZNYj7326LoecpmuJ8lbAHwPgK8WkV9s4NtRriNwia/nlCm4MWzYtF9HyLRhl08mKTu7bO6JRfKe4dxHo8Q7XEZ5KYCPqd8/pv6+dHJZrmcddvh2AD8gIj8wOOVKXM8pU3Bj2LAr0d4u07WcNuxyytxm6RyE5N8D8PdR98RCGZt/rIg8neSXA3hMPfbLAD5TRP7sOpXzWwB8CIBbAfxXAE8N5bwbgGcBeGcAvw3g00XkTy5hOS/F9ST5t1D2O3tVPfQLAH4IwP1E5Lkk7wPg2wC8A4CfB/BEETm96HJOmbKPXAUbNu3XuZdz2rBLKJOUTZkyZcqUKVOmXAKZw5dTpkyZMmXKlCmXQCYpmzJlypQpU6ZMuQQySdmUKVOmTJkyZcolkEnKpkyZMmXKlClTLoFMUjZlyiWSrX3zDkjj3UjeHvbX+6vnWcYpU6ZMGcm0X2eXu1zvAkyZMiXJZ6BMmX/nM6bzX0TkMedQnilTpkzZV6b9OqNMT9mUKZdI2n3zSN6D5PeS/HGSLyL5nnsm9QEkX0by2+p6Q1OmTJlyTWXar7PLJGVTAAAkP4zkm0m+muR/I/likg84QF9I/jzJc+3dkHwhyTeS/OzzTPcKyVMAPE9EPhLAPwbwVXvovAHAu4vIhwF4OYB/dQ3LN2XKdZdpvy6tTPt1oMzhyykqHwjg/xaRv0OSAF4I4HNQGtW+8iEi8ubzLJSIPIbks88zzSsm7wfgw0n+w/r7rQBA8qkAPro9WUQeUbfI0W1yvh3AZ11AOadMuZ4y7dfllGm/DpRJyqaofCCA1wKAiAjJX4M3jIOF5LcBeKGIfFv9/Y0AXisi30RSAHwJgL8O4D4A/gFKHMJHA7gFwCeIyOvOUJcbSV4L4KdE5PsBgOTbAICIPBVl+5ZOSN5DRN5Uf17WvQqnTDlPmfbrcsq0XwfKHL6covKBAH4RAEi+L4CHAPjGM6T3QfA91Ua/7xCRvwTgyQCeD+AnReQvouy19k/PkO+Vlrpv3hcAeDzJHwDw5QD+do3HeDFK73+XfATJnyP5UgBPBPCka1bgKVMuh0z7dQlk2q+zy9z7cgpI3hXAHwH4FZSe3jsD+GgReSnJEwAvA/B6AA8D8PEi8kuDNATA3UXkzSTfDsDvAriXiJzWNN4I4P4i8if13PuJyG0k3wPAq0Xk7jWdjwTw5SLyiJD2swG8SkS+4ZpdhClTplxJ2WG/iLKZ9u/W439HRF4zSGParymXQqanbAoAfACA20XkfUXkvQD8O7hr+T0B3Ang76L0ev7SHun9RQC/ICKn9fdDAbxeRP4knKPf70QeZrgTc1h9ypQp+8uW/Xowin35awA+G8An7JHetF9TrpvMh2cKUFz/Pxt+/2sAv0HyfgDeB8B3ishbSd4bwH/ZI72HAbiV5ILSc/1iAK8+3yJPmTJlCoBt+/W+AJ4jIm8heSeA2/dIb9qvKddNpqdsClCM2s/oDxH5XQA/jdK7fF8A6u5/P9S4jR3yQQB+C8WQvRjArwN4NMl3P78iT5kyZQqA3fZL5fEAfmSP9Kb9mnLdZMaUTdmUOgvps0Xkf5L8f0Tk41bOizEZrwPwN0axZ0eW4dmYMRlTpkw5UEg+C8CbULxfzxORf7ty3rRfUy6FzOHLKZsiIp8Wvg8JWZX/AeAnSf5TAPfHOU1jJvlCAO8B4KXnkd6UKVNuKrmniPy9Pc6b9mvKpZDpKZsyZcqUKTekkHy+iHz89S7HlCn7yiRlU6ZMmTJlypQpl0BmoP+UKVOmTJkyZcolkEnKpkyZMmXKlClTLoFMUjZlypQpU6ZMmXIJZJKyKVOmTJkyZcqUSyCTlE2ZMmXKlClTplwCmaRsypQpU6ZMmTLlEsgkZVOmTJkyZcqUKZdAJimbMmXKlClTpky5BDJJ2ZQpU6ZMmTJlyiWQ/xepm9NruYLJ/AAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# final time\n", + "plot_concentrations(t[-1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Input custom particle-size distributions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to solve the MPM, one must input the area-weighted particle-size distribution $f_{\\text{k},a}$ for each electrode $\\text{k}=\\text{n,p}$ and the minimum and maximum radius limits $R_\\text{k,min}$, $R_\\text{k,max}$. The default distributions $f_{\\text{k},a}$, usable with the Marquis et al. [[5]](#References) parameter set, are lognormals with means equal to the `\"Negative particle radius [m]\"` and `\"Positive particle radius [m]\"` values, and standard deviations equal to 0.3 times the mean.\n", + "\n", + "You can input any size distribution $f_{\\text{k},a}(R_\\text{k})$ as a function of $R_\\text{k}$, which we will now demonstrate.\n", + "\n", + "Note: $f_{\\text{k},a}(R_\\text{k})$ should ideally integrate to 1 over the specified $R_\\text{k}$ range, although it is automatically normalized within PyBaMM anyway. A distribution such as a lognormal, once restricted to $[R_\\text{k,min},R_\\text{k,max}]$, discretized, and then renormalized, strictly will not integrate to 1 or have the originally desired mean or variance. The mean and variance of the final discretized distribution can be checked as output variables (see below). Having a sufficient number of mesh points in $R_\\text{k}$ or a sufficiently wide interval $[R_\\text{k,min},R_\\text{k,max}]$ should alleviate this issue, however." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a lognormal distribution\n", + "def lognormal(R, R_av, sd):\n", + " '''\n", + " A lognormal distribution with arguments\n", + " R : particle radius\n", + " R_av: mean particle radius\n", + " sd : standard deviation\n", + " '''\n", + " # calculate usual lognormal parameters\n", + " mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2))\n", + " sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2))\n", + " return (\n", + " pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2))\n", + " / pybamm.sqrt(2 * np.pi * sigma_ln ** 2)\n", + " / R\n", + " )\n", + "\n", + "\n", + "# Parameter set (no distribution parameters by default)\n", + "params = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019)\n", + "\n", + "# Extract the radii values. We will choose these to be the means of our area-weighted distributions\n", + "R_a_n_dim = params[\"Negative particle radius [m]\"]\n", + "R_a_p_dim = params[\"Positive particle radius [m]\"]\n", + "\n", + "# Standard deviations (dimensional)\n", + "sd_a_n_dim = 0.2 * R_a_n_dim \n", + "sd_a_p_dim = 0.6 * R_a_p_dim\n", + "\n", + "# Minimum and maximum particle sizes (dimensional)\n", + "R_min_n = 0\n", + "R_min_p = 0\n", + "R_max_n = 2 * R_a_n_dim\n", + "R_max_p = 3 * R_a_p_dim\n", + "\n", + "# Set the area-weighted particle-size distributions\n", + "# Note: the only argument must be the particle size R\n", + "def f_a_dist_n_dim(R):\n", + " return lognormal(R, R_a_n_dim, sd_a_n_dim)\n", + "\n", + "\n", + "def f_a_dist_p_dim(R):\n", + " return lognormal(R, R_a_p_dim, sd_a_p_dim)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# input distribution params to the dictionary\n", + "distribution_params = {\n", + " \"Negative minimum particle radius [m]\": R_min_n,\n", + " \"Positive minimum particle radius [m]\": R_min_p,\n", + " \"Negative maximum particle radius [m]\": R_max_n,\n", + " \"Positive maximum particle radius [m]\": R_max_p,\n", + " \"Negative area-weighted \"\n", + " + \"particle-size distribution [m-1]\": f_a_dist_n_dim,\n", + " \"Positive area-weighted \"\n", + " + \"particle-size distribution [m-1]\": f_a_dist_p_dim,\n", + "}\n", + "params.update(distribution_params, check_already_exists=False)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2bdb2f4023e04f168de4277d27e44285", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=3511.0164027810006, step=35.11016402781001),…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim = pybamm.Simulation(model, parameter_values=params)\n", + "sim.solve(t_eval=[0, 3600])\n", + "\n", + "sim.plot(output_variables=output_variables)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The discretized size distributions can be plotted as histograms. Only the area-weighted distribution has been input, but the corresponding number and volume-weighted ones are also given as output variables." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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cNppp1FWVsZf/vIXBg8ezOrVq3nkkUdo06YNxcXFWdPPR6tWrYDwW5svvvgi4zruzmOPPUa3bt1qvZ+GJtEmODNrD1wGDAHOB8amrfIWYajhbca2d/cL3H0IMAwoA6bFReXuPiT+KfiINHFr166lffv27Lrrrrzzzjv87W9/226d448/nqlTp/LJJ58AsHr1at5//30+++wzdtttN9q2bcvHH3/Ms88+C8ARRxxBaWkppaWlnHbaaQAMHDiQ22+/ncGDBzNo0CBuu+02Bg0aVG36qY466iieeuopNm3axPr163n66adrLFtxcTHr1q2rmh42bBjjxo2jchTr119/HYDBgwfz0EMPAfDmm29mbEZsqJKuAQ0AZrr7FmCJmRWbWSt33wzg7muh2h9AnQL8tXL9sKq9BGwGrnL30up2bmajCLUvOnXqVN2qIlKDJB6bPumkk7j77rvp3r073bp1Y+DAgdut06NHD2688UZOPPFEKioqaNmyJb/73e8YOHAgffv25atf/SodO3bkqKOOyrqfQYMGMW3aNA466CA6d+7M6tWrqwJQtvQ7d+5ctf3hhx/OaaedRu/evdl3333p1asXbdu2rbZsxx57LDfffDMlJSWMGTOGX/7yl1xxxRX07t2biooKunbtytNPP80ll1zChRdeSPfu3enevTuHHXZYLY9m/bPKaJrIzs2+DRzi7tfF6RnAue6+Im296cD5lU1wKfMfB8a5+0txei93X2VmfYBJ7n5ornnp37+/Jz0gnR7DlsZi69atQOgjTHKzfv162rRpw+eff87gwYMZP348/fr1SzpbOct2zs1sgbvX6kdHSdeAVgPtUqbbxnk1MrN2QC9geuU8d18V/y80s8/NrL27rylUZkVEamvUqFEsWrSITZs28d3vfrdRBZ+6knQAmgPcaGYtgf2B9SnNaTU5B3jcYxXOzFoRanSbzKwDIbCVFT7LIiL5q7xPI19K9CGEWDu5E5gBTAauMLMSMxsNYGaHmNkLhCfdJpvZJSmbnw88mDK9D/Cqmc0EHgUu9iTbF0VEpFpJ14Bw9wnAhLTZpXHZP4ATsmw3OG16GaA6rYhII6GeEEREJBEKQCIikojEm+BEpGnI52cEuUjypwYNZUiDpj6cg2pAIiIFlK0rndp45plnqg0+1SktLd2h4SDqgwKQiDRaS5cupXv37vzgBz+g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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "# The discrete sizes or \"bins\" used, and the distributions\n", + "R_p = sim.solution[\"Positive particle sizes [m]\"].entries[:,0] # const in the current collector direction\n", + "# The distributions\n", + "f_a_p = sim.solution[\"X-averaged positive area-weighted particle-size distribution [m-1]\"].entries[:,0]\n", + "f_num_p = sim.solution[\"X-averaged positive number-based particle-size distribution [m-1]\"].entries[:,0]\n", + "f_v_p = sim.solution[\"X-averaged positive volume-weighted particle-size distribution [m-1]\"].entries[:,0]\n", + "\n", + "\n", + "# plot\n", + "width_p = (R_p[-1] - R_p[-2])/ 1e-6\n", + "plt.bar(R_p / 1e-6, f_a_p * 1e-6, width=width_p, alpha=0.3, color=\"tab:blue\",\n", + " label=\"area-weighted\")\n", + "plt.bar(R_p / 1e-6, f_num_p * 1e-6, width=width_p, alpha=0.3, color=\"tab:red\",\n", + " label=\"number-weighted\")\n", + "plt.bar(R_p / 1e-6, f_v_p * 1e-6, width=width_p, alpha=0.3, color=\"tab:green\",\n", + " label=\"volume-weighted\")\n", + "plt.xlim((0,30))\n", + "plt.xlabel(\"Particle size $R_{\\mathrm{p}}$ [$\\mu$m]\", fontsize=12)\n", + "plt.ylabel(\"[$\\mu$m$^{-1}$]\", fontsize=12)\n", + "plt.legend(fontsize=10)\n", + "plt.title(\"Discretized distributions (histograms) in positive electrode\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Vary standard deviation as an input parameter\n", + "You may define the standard deviation (or other distribution parameters except for the min or max radii) of the distribution as a pybamm \"input\" parameter, to quickly change the distribution at the solve stage." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "# Define standard deviation in negative electrode to vary\n", + "sd_a_p_dim = pybamm.Parameter(\"Positive electrode area-weighted particle-size standard deviation [m]\")\n", + "\n", + "# Set the area-weighted particle-size distribution\n", + "def f_a_dist_p_dim(R):\n", + " return lognormal(R, R_a_p_dim, sd_a_p_dim)\n", + "\n", + "# input to param dictionary\n", + "distribution_params = {\n", + " \"Positive electrode area-weighted particle-size \"\n", + " + \"standard deviation [m]\": \"[input]\",\n", + " \"Positive area-weighted \"\n", + " + \"particle-size distribution [m-1]\": f_a_dist_p_dim,\n", + "}\n", + "params.update(distribution_params, check_already_exists=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5fc4af340c79406a90d0fecaf0f1b5c6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.9444444444444444, step=0.01944444444444444…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Experiment with a relaxation period, to see the effect of distribution width\n", + "experiment = pybamm.Experiment([\"Discharge at 1 C for 3400 s\", \"Rest for 1 hours\"])\n", + "\n", + "sim = pybamm.Simulation(model, parameter_values=params, experiment=experiment)\n", + "solutions = []\n", + "for sd_a_p in [0.4, 0.6, 0.8]: \n", + " solution = sim.solve(\n", + " inputs={\n", + " \"Positive electrode area-weighted particle-size \"\n", + " + \"standard deviation [m]\": sd_a_p * R_a_p_dim\n", + " }\n", + " )\n", + " solutions.append(solution)\n", + "\n", + "\n", + "pybamm.dynamic_plot(\n", + " solutions,\n", + " output_variables=output_variables,\n", + " labels=[\"MPM, sd_a_p=0.4\", \"MPM, sd_a_p=0.6\", \"MPM, sd_a_p=0.8\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check the distribution statistics\n", + "The mean and standard deviations of the final discretized distributions can be investigated using the output variables `\"Negative area-weighted mean particle radius\"` and `\"Negative area-weighted particle-size standard deviation\"`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The mean of the input lognormal was: 1e-05\n", + "The means of discretized distributions are:\n", + "Positive area-weighted mean particle radius [m] 9.972515783613799e-06\n", + "Positive area-weighted mean particle radius [m] 9.673853099212895e-06\n", + "Positive area-weighted mean particle radius [m] 9.124186918191047e-06\n" + ] + } + ], + "source": [ + "print(\"The mean of the input lognormal was:\", R_a_p_dim)\n", + "print(\"The means of discretized distributions are:\") \n", + "for solution in solutions:\n", + " R = solution[\"Positive area-weighted mean particle radius [m]\"]\n", + " print(\"Positive area-weighted mean particle radius [m]\", R.entries[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The standard deviations of the input lognormal were:\n", + "4.000000000000001e-06\n", + "6e-06\n", + "8.000000000000001e-06\n", + "The standard deviations of discretized distributions are:\n", + "Positive area-weighted particle-size standard deviation [m] 3.918218937679725e-06\n", + "Positive area-weighted particle-size standard deviation [m] 5.180362201055076e-06\n", + "Positive area-weighted particle-size standard deviation [m] 5.815728559306213e-06\n" + ] + } + ], + "source": [ + "print(\"The standard deviations of the input lognormal were:\")\n", + "print(0.4 * R_a_p_dim)\n", + "print(0.6 * R_a_p_dim)\n", + "print(0.8 * R_a_p_dim)\n", + "print(\"The standard deviations of discretized distributions are:\") \n", + "for solution in solutions:\n", + " sd = solution[\"Positive area-weighted particle-size standard deviation [m]\"]\n", + " print(\"Positive area-weighted particle-size standard deviation [m]\", sd.entries[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compare to SPM and DFN\n", + "The MPM can also be easily compared to PyBaMM models with a single particle size. The standard output variables are computed in the MPM, averaging over the particle size domain." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "798ffd68d39b46cfa844775343b2ecab", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=3500.0, step=35.0), Output()), _dom_classes=…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "models = [\n", + " pybamm.lithium_ion.SPM(),\n", + " pybamm.lithium_ion.MPM(),\n", + " pybamm.lithium_ion.DFN()\n", + "]\n", + "\n", + "# solve\n", + "sims = []\n", + "for model in models:\n", + " sim = pybamm.Simulation(model)\n", + " sim.solve(t_eval=[0, 3500])\n", + " sims.append(sim)\n", + "\n", + "# plot\n", + "pybamm.dynamic_plot(sims)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model options\n", + "The MPM is compatible with the current collector and thermal models (except the \"x-full\" thermal option). Currently, the MPM is not compatible with the various degradation submodels in PyBaMM (i.e. SEI models, particle cracking/swelling, or lithium plating)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fickian diffusion vs Uniform profile\n", + "One can choose from Fickian diffusion or a uniform concentration profile within the particles. Teh default is \"Fickian diffusion\"." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c46f8ef55de14c2aab5276ea4f10addb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=3500.0, step=35.0), Output()), _dom_classes=…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_Fickian = pybamm.lithium_ion.MPM(name=\"MPM Fickian\")\n", + "model_Uniform = pybamm.lithium_ion.MPM(\n", + " name=\"MPM Uniform\",\n", + " options={\"particle\": \"uniform profile\"}\n", + ")\n", + "\n", + "sim_Fickian = pybamm.Simulation(model_Fickian)\n", + "sim_Uniform = pybamm.Simulation(model_Uniform)\n", + "\n", + "sim_Fickian.solve(t_eval=[0, 3500])\n", + "sim_Uniform.solve(t_eval=[0, 3500])\n", + "\n", + "pybamm.dynamic_plot([sim_Fickian, sim_Uniform], output_variables=output_variables)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1D current collector model\n", + "Add another macroscale dimension \"z\", employing the \"potential pair\" option solving for the potential in the current collectors." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "924fda69874e424eb1c5cc059ceaea52", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# choose model options\n", + "model_cc = pybamm.lithium_ion.MPM(\n", + " options={\n", + " \"current collector\": \"potential pair\",\n", + " \"dimensionality\": 1,\n", + " \"particle\": \"uniform profile\", # to reduce computation time\n", + " }\n", + ")\n", + "\n", + "# solve\n", + "sim_cc = pybamm.Simulation(model_cc)\n", + "sim_cc.solve(t_eval=[0, 3600])\n", + "\n", + "# variables to plot\n", + "output_variables = [\n", + " \"X-averaged negative particle surface concentration distribution\",\n", + " \"X-averaged positive particle surface concentration distribution\",\n", + " \"X-averaged positive electrode interfacial current density distribution\",\n", + " \"Negative current collector potential [V]\",\n", + " \"Positive current collector potential [V]\",\n", + " \"Terminal voltage [V]\",\n", + "]\n", + "pybamm.dynamic_plot(sim_cc, output_variables=output_variables)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "The relevant papers for this notebook are:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", + "[2] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", + "[3] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", + "[4] Toby L. Kirk, Jack Evans, Colin P. Please, and S. Jonathan Chapman. Modelling electrode heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size distributions. arXiv:2006.12208, 2020. URL: https://arxiv.org/abs/2006.12208, arXiv:2006.12208.\n", + "[5] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[6] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", + "[7] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.\n", + "[8] Robert Timms, Scott G. Marquis, Valentin Sulzer, Colin P. Please, and S. Jon Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. Submitted for publication, 2020. arXiv:2005.05127.\n", + "[9] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", + "\n" + ] + } + ], + "source": [ + "pybamm.print_citations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 683674c16c16a63b27091ec5fc84a3e8ee050ac4 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 16:29:50 +0100 Subject: [PATCH 45/67] removed the 'ManyDistributions' submodels --- .../fast_many_distributions.rst | 5 - .../fickian_many_distributions.rst | 7 - .../particle/size_distribution/index.rst | 2 - examples/notebooks/models/MPM.ipynb | 79 +++--- .../submodels/electrode/ohm/__init__.py | 1 - .../ohm/leading_size_distribution_ohm.py | 166 ------------ .../particle/size_distribution/__init__.py | 2 - .../fast_many_distributions.py | 174 ------------ .../fickian_many_distributions.py | 254 ------------------ 9 files changed, 40 insertions(+), 650 deletions(-) delete mode 100644 docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst delete mode 100644 docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst delete mode 100644 pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py delete mode 100644 pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py delete mode 100644 pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py diff --git a/docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst b/docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst deleted file mode 100644 index 97f0feed64..0000000000 --- a/docs/source/models/submodels/particle/size_distribution/fast_many_distributions.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fast Many Size Distributions -============================ - -.. autoclass:: pybamm.particle.FastManySizeDistributions - :members: diff --git a/docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst b/docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst deleted file mode 100644 index 7b927b52cc..0000000000 --- a/docs/source/models/submodels/particle/size_distribution/fickian_many_distributions.rst +++ /dev/null @@ -1,7 +0,0 @@ -Fickian Many Size Distributions -=============================== - -.. autoclass:: pybamm.particle.FickianManySizeDistributions - :members: - - diff --git a/docs/source/models/submodels/particle/size_distribution/index.rst b/docs/source/models/submodels/particle/size_distribution/index.rst index 1efda8f7cd..aeed4cd7c3 100644 --- a/docs/source/models/submodels/particle/size_distribution/index.rst +++ b/docs/source/models/submodels/particle/size_distribution/index.rst @@ -6,6 +6,4 @@ Particle Size Distribution base_distribution fickian_single_distribution - fickian_many_distributions fast_single_distribution - fast_many_distributions diff --git a/examples/notebooks/models/MPM.ipynb b/examples/notebooks/models/MPM.ipynb index 3ae0f50738..ab8d107e9c 100644 --- a/examples/notebooks/models/MPM.ipynb +++ b/examples/notebooks/models/MPM.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The Many Paticle Model (MPM) of a lithium-ion battery is an extension of the Single Particle Model to account for a continuous distribution of active particle sizes in each electrode $\\text{k}=\\text{n},\\text{p}$. Therefore, many of the same model assumptions hold, e.g., the transport in the electrolyte is instantaneous and hence the through-cell variation (in $x$) is neglected. The full set of assumptions and description of the particle size geometry is given in [[4]](#References). Note that the MPM in [[4]](#References) is for a half cell and the version implemented in PyBaMM is for a full cell and uses the notation and scaling given in [[??]](#References).\n", + "The Many Paticle Model (MPM) of a lithium-ion battery is an extension of the Single Particle Model to account for a continuous distribution of active particle sizes in each electrode $\\text{k}=\\text{n},\\text{p}$. Therefore, many of the same model assumptions hold, e.g., the transport in the electrolyte is instantaneous and hence the through-cell variation (in $x$) is neglected. The full set of assumptions and description of the particle size geometry is given in [[4]](#References). Note that the MPM in [[4]](#References) is for a half cell and the version implemented in PyBaMM is for a full cell and uses the notation and scaling given in [[5]](#References).\n", "\n", "\n", "## Particle size geometry\n", @@ -78,7 +78,7 @@ "$$\n", "\n", "### Dimensionless equations\n", - "The dimensionless scheme can be found in the appendix of [[??]](#References), giving similar dimensionless variables and parameters to those in the SPM." + "The dimensionless scheme can be found in the appendix of [[5]](#References), giving similar dimensionless variables and parameters to those in the SPM." ] }, { @@ -298,7 +298,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "bf100c6314c74d7c9ca92ee5379f5324", + "model_id": "fb2cf1a877d243cea4aa5eae1144b78b", "version_major": 2, "version_minor": 0 }, @@ -312,7 +312,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 7, @@ -346,7 +346,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -403,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -435,7 +435,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In order to solve the MPM, one must input the area-weighted particle-size distribution $f_{\\text{k},a}$ for each electrode $\\text{k}=\\text{n,p}$ and the minimum and maximum radius limits $R_\\text{k,min}$, $R_\\text{k,max}$. The default distributions $f_{\\text{k},a}$, usable with the Marquis et al. [[5]](#References) parameter set, are lognormals with means equal to the `\"Negative particle radius [m]\"` and `\"Positive particle radius [m]\"` values, and standard deviations equal to 0.3 times the mean.\n", + "In order to solve the MPM, one must input the area-weighted particle-size distribution $f_{\\text{k},a}$ for each electrode $\\text{k}=\\text{n,p}$ and the minimum and maximum radius limits $R_\\text{k,min}$, $R_\\text{k,max}$. The default distributions $f_{\\text{k},a}$, usable with the Marquis et al. [[6]](#References) parameter set, are lognormals with means equal to the `\"Negative particle radius [m]\"` and `\"Positive particle radius [m]\"` values, and standard deviations equal to 0.3 times the mean.\n", "\n", "You can input any size distribution $f_{\\text{k},a}(R_\\text{k})$ as a function of $R_\\text{k}$, which we will now demonstrate.\n", "\n", @@ -444,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -496,7 +496,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -516,13 +516,13 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2bdb2f4023e04f168de4277d27e44285", + "model_id": "5dff78a1e2bc4a1884d5811ff443897c", "version_major": 2, "version_minor": 0 }, @@ -536,10 +536,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 18, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -560,7 +560,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -612,7 +612,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -635,13 +635,13 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5fc4af340c79406a90d0fecaf0f1b5c6", + "model_id": "1d8c9493b65344a580dbe0cd7fef4081", "version_major": 2, "version_minor": 0 }, @@ -655,10 +655,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 21, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -696,7 +696,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -721,7 +721,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -760,13 +760,13 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "798ffd68d39b46cfa844775343b2ecab", + "model_id": "30e5982a7e1345d7912e3aa131fa6ff7", "version_major": 2, "version_minor": 0 }, @@ -780,10 +780,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 24, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -824,13 +824,13 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c46f8ef55de14c2aab5276ea4f10addb", + "model_id": "6938c3cab8f14709985552080e01355e", "version_major": 2, "version_minor": 0 }, @@ -844,10 +844,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 28, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -878,13 +878,13 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "924fda69874e424eb1c5cc059ceaea52", + "model_id": "0ff76ec97d994aed8415b4088e84b925", "version_major": 2, "version_minor": 0 }, @@ -898,10 +898,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 35, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -943,7 +943,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -954,11 +954,12 @@ "[2] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", "[3] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", "[4] Toby L. Kirk, Jack Evans, Colin P. Please, and S. Jonathan Chapman. Modelling electrode heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size distributions. arXiv:2006.12208, 2020. URL: https://arxiv.org/abs/2006.12208, arXiv:2006.12208.\n", - "[5] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", - "[6] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", - "[7] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.\n", - "[8] Robert Timms, Scott G. Marquis, Valentin Sulzer, Colin P. Please, and S. Jon Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. Submitted for publication, 2020. arXiv:2005.05127.\n", - "[9] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", + "[5] Toby L. Kirk, Colin P. Please, and S. Jon Chapman. Physical modelling of the slow voltage relaxation phenomenon in lithium-ion batteries. Journal of The Electrochemical Society, 168(6):060554, jun 2021. URL: https://doi.org/10.1149/1945-7111/ac0bf7, doi:10.1149/1945-7111/ac0bf7.\n", + "[6] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[7] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", + "[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.\n", + "[9] Robert Timms, Scott G. Marquis, Valentin Sulzer, Colin P. Please, and S. Jon Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. Submitted for publication, 2020. arXiv:2005.05127.\n", + "[10] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", "\n" ] } diff --git a/pybamm/models/submodels/electrode/ohm/__init__.py b/pybamm/models/submodels/electrode/ohm/__init__.py index c7a788eca3..4d684769f3 100644 --- a/pybamm/models/submodels/electrode/ohm/__init__.py +++ b/pybamm/models/submodels/electrode/ohm/__init__.py @@ -2,5 +2,4 @@ from .composite_ohm import Composite from .full_ohm import Full from .leading_ohm import LeadingOrder -from .leading_size_distribution_ohm import LeadingOrderSizeDistribution from .surface_form_ohm import SurfaceForm diff --git a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py deleted file mode 100644 index 5adf37d609..0000000000 --- a/pybamm/models/submodels/electrode/ohm/leading_size_distribution_ohm.py +++ /dev/null @@ -1,166 +0,0 @@ -# -# Full model for Ohm's law in the electrode -# -import pybamm - -from .base_ohm import BaseModel - - -class LeadingOrderSizeDistribution(BaseModel): - """An electrode submodel that employs Ohm's law the leading-order approximation - (no variation in x) to governing equations when there is a distribution of particle - sizes. An algebraic equation is imposed for the x-averaged surface potential - difference. - - Parameters - ---------- - param : parameter class - 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 positive potential based on the current. - If False, the potential is specified by the user. Default is True. - - **Extends:** :class:`pybamm.electrode.ohm.BaseModel` - """ - - def __init__(self, param, domain, set_positive_potential=True): - super().__init__(param, domain, set_positive_potential=set_positive_potential) - - def get_fundamental_variables(self): - - delta_phi_av = pybamm.Variable( - "X-averaged " - + self.domain.lower() - + " electrode surface potential difference", - domain="current collector", - ) - variables = self._get_standard_surface_potential_difference_variables( - delta_phi_av - ) - - return variables - - def get_coupled_variables(self, variables): - - i_boundary_cc = variables["Current collector current density"] - phi_s_cn = variables["Negative current collector potential"] - - # import parameters and spatial variables - l_n = self.param.l_n - l_p = self.param.l_p - x_n = pybamm.standard_spatial_vars.x_n - x_p = pybamm.standard_spatial_vars.x_p - - if self.domain == "Negative": - phi_s = pybamm.PrimaryBroadcast(phi_s_cn, ["negative electrode"]) - i_s = i_boundary_cc * (1 - x_n / l_n) - - elif self.domain == "Positive": - # recall delta_phi = phi_s - phi_e - delta_phi_p_av = variables[ - "X-averaged positive electrode surface potential difference" - ] - phi_e_p_av = variables["X-averaged positive electrolyte potential"] - - v = delta_phi_p_av + phi_e_p_av - - phi_s = pybamm.PrimaryBroadcast(v, ["positive electrode"]) - i_s = i_boundary_cc * (1 - (1 - x_p) / l_p) - - variables.update(self._get_standard_potential_variables(phi_s)) - variables.update(self._get_standard_current_variables(i_s)) - - if self.domain == "Positive": - variables.update(self._get_standard_whole_cell_variables(variables)) - - return variables - - def set_algebraic(self, variables): - - j_tot_av = variables[ - "X-averaged " - + self.domain.lower() - + " electrode total interfacial current density" - ] - - # Extract total sum of interfacial current densities - sum_j_av = variables[ - "Sum of x-averaged " - + self.domain.lower() - + " electrode interfacial current densities" - ] - delta_phi_av = variables[ - "X-averaged " - + self.domain.lower() - + " electrode surface potential difference" - ] - # Algebraic equation for the (X-avg) surface potential difference phi_s - phi_e. - # The electrode total interfacial current density (already integrated across - # particle size) must equal the sum from all sources, sum_j_av. May not account - # for interfacial current densities from reactions other than "main" - self.algebraic[delta_phi_av] = sum_j_av - j_tot_av - - def set_initial_conditions(self, variables): - - delta_phi_av = variables[ - "X-averaged " - + self.domain.lower() - + " electrode surface potential difference" - ] - T_init = self.param.T_init - - if self.domain == "Negative": - delta_phi_av_init = self.param.U_n(self.param.c_n_init(0), T_init) - elif self.domain == "Positive": - delta_phi_av_init = self.param.U_p( - self.param.c_p_init(1), T_init - ) - - self.initial_conditions[delta_phi_av] = delta_phi_av_init - - def set_boundary_conditions(self, variables): - - phi_s = variables[self.domain + " electrode potential"] - - lbc = (pybamm.Scalar(0), "Neumann") - rbc = (pybamm.Scalar(0), "Neumann") - - self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - - def _get_standard_surface_potential_difference_variables(self, delta_phi): - - if self.domain == "Negative": - ocp_ref = self.param.U_n_ref - elif self.domain == "Positive": - ocp_ref = self.param.U_p_ref - pot_scale = self.param.potential_scale - - # Average, and broadcast if necessary - if delta_phi.domain == []: - delta_phi_av = delta_phi - delta_phi = pybamm.FullBroadcast( - delta_phi, self.domain_for_broadcast, "current collector" - ) - elif delta_phi.domain == ["current collector"]: - delta_phi_av = delta_phi - delta_phi = pybamm.PrimaryBroadcast(delta_phi, self.domain_for_broadcast) - else: - delta_phi_av = pybamm.x_average(delta_phi) - - variables = { - self.domain + " electrode surface potential difference": delta_phi, - "X-averaged " - + self.domain.lower() - + " electrode surface potential difference": delta_phi_av, - self.domain - + " electrode surface potential difference [V]": ocp_ref - + delta_phi * pot_scale, - "X-averaged " - + self.domain.lower() - + " electrode surface potential difference [V]": ocp_ref - + delta_phi_av * pot_scale, - } - - return variables diff --git a/pybamm/models/submodels/particle/size_distribution/__init__.py b/pybamm/models/submodels/particle/size_distribution/__init__.py index 6ef02d5297..a1ccddb609 100644 --- a/pybamm/models/submodels/particle/size_distribution/__init__.py +++ b/pybamm/models/submodels/particle/size_distribution/__init__.py @@ -1,5 +1,3 @@ from .base_distribution import BaseSizeDistribution -from .fickian_many_distributions import FickianManySizeDistributions from .fickian_single_distribution import FickianSingleSizeDistribution -from .fast_many_distributions import FastManySizeDistributions from .fast_single_distribution import FastSingleSizeDistribution diff --git a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py deleted file mode 100644 index 6815af81fd..0000000000 --- a/pybamm/models/submodels/particle/size_distribution/fast_many_distributions.py +++ /dev/null @@ -1,174 +0,0 @@ -# -# Class for many particle-size distributions, one distribution at every -# x location of the electrode, with fast diffusion (uniform concentration in r) -# within particles -# -import pybamm - -from .base_distribution import BaseSizeDistribution - - -class FastManySizeDistributions(BaseSizeDistribution): - """Class for molar conservation in many particle-size - distributions, one distribution at every x location of the electrode, - with fast diffusion (uniform concentration in r) within the particles - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - domain : str - The domain of the model either 'Negative' or 'Positive' - - - **Extends:** :class:`pybamm.particle.BaseSizeDistribution` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - pybamm.citations.register("Kirk2021") - - def get_fundamental_variables(self): - # The concentration is uniform throughout each particle, so we - # can just use the surface value. - - if self.domain == "Negative": - # distribution variables - c_s_surf_distribution = pybamm.Variable( - "Negative particle surface concentration distribution", - domain="negative particle size", - auxiliary_domains={ - "secondary": "negative electrode", - "tertiary": "current collector", - }, - bounds=(0, 1), - ) - R = pybamm.standard_spatial_vars.R_n - - elif self.domain == "Positive": - # distribution variables - c_s_surf_distribution = pybamm.Variable( - "Positive particle surface concentration distribution", - domain="positive particle size", - auxiliary_domains={ - "secondary": "positive electrode", - "tertiary": "current collector", - }, - bounds=(0, 1), - ) - R = pybamm.standard_spatial_vars.R_p - - # Distribution variables - variables = self._get_distribution_variables(R) - - # Flux variables (zero) - N_s = pybamm.FullBroadcastToEdges( - 0, - [self.domain.lower() + " particle"], - auxiliary_domains={ - "secondary": self.domain.lower() + " electrode", - "tertiary": "current collector", - }, - ) - N_s_xav = pybamm.FullBroadcast( - 0, self.domain.lower() + " electrode", "current collector" - ) - - # Standard concentration distribution variables (R-dependent) - variables.update( - self._get_standard_concentration_distribution_variables( - c_s_surf_distribution - ) - ) - - # Standard R-averaged variables. Average concentrations using - # the volume-weighted distribution since they are volume-based - # quantities. Necessary for output variables "Total lithium in - # negative electrode [mol]", etc, to be calculated correctly - f_v_dist = variables[ - self.domain + " volume-weighted particle-size distribution" - ] - c_s_surf = pybamm.Integral(f_v_dist * c_s_surf_distribution, R) - c_s = pybamm.PrimaryBroadcast( - c_s_surf, [self.domain.lower() + " particle"] - ) - c_s_xav = pybamm.x_average(c_s) - variables.update(self._get_standard_concentration_variables(c_s, c_s_xav)) - variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) - return variables - - def get_coupled_variables(self, variables): - variables.update(self._get_total_concentration_variables(variables)) - return variables - - def set_rhs(self, variables): - c_s_surf_distribution = variables[ - self.domain - + " particle surface concentration distribution" - ] - j_distribution = variables[ - self.domain - + " electrode interfacial current density distribution" - ] - R = variables[self.domain + " particle sizes"] - - if self.domain == "Negative": - self.rhs = { - c_s_surf_distribution: -3 - * j_distribution - / self.param.a_R_n - / R - } - elif self.domain == "Positive": - self.rhs = { - c_s_surf_distribution: -3 - * j_distribution - / self.param.a_R_p - / self.param.gamma_p - / R - } - - def set_initial_conditions(self, variables): - c_s_surf_distribution = variables[ - self.domain - + " particle surface concentration distribution" - ] - - if self.domain == "Negative": - # Broadcast x_n to particle size domain - x_n = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_n, "negative particle size" - ) - c_init = self.param.c_n_init(x_n) - - elif self.domain == "Positive": - # Broadcast x_p to particle size domain - x_p = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_p, "positive particle size" - ) - c_init = self.param.c_p_init(x_p) - - self.initial_conditions = {c_s_surf_distribution: c_init} - - def set_events(self, variables): - c_s_surf_distribution = variables[ - self.domain - + " particle surface concentration distribution" - ] - tol = 1e-4 - - self.events.append( - pybamm.Event( - "Minumum " + self.domain.lower() + " particle surface concentration", - pybamm.min(c_s_surf_distribution) - tol, - pybamm.EventType.TERMINATION, - ) - ) - - self.events.append( - pybamm.Event( - "Maximum " + self.domain.lower() + " particle surface concentration", - (1 - tol) - pybamm.max(c_s_surf_distribution), - pybamm.EventType.TERMINATION, - ) - ) diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py b/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py deleted file mode 100644 index 2d80455c70..0000000000 --- a/pybamm/models/submodels/particle/size_distribution/fickian_many_distributions.py +++ /dev/null @@ -1,254 +0,0 @@ -# -# Class for many particle-size distributions, one distribution at every -# x location of the electrode, and Fickian diffusion within each particle -# -import pybamm - -from .base_distribution import BaseSizeDistribution - - -class FickianManySizeDistributions(BaseSizeDistribution): - """Class for molar conservation in many particle-size - distributions, one distribution at every x location of the electrode, - with Fickian diffusion within each particle. - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - domain : str - The domain of the model either 'Negative' or 'Positive' - - - **Extends:** :class:`pybamm.particle.BaseSizeDistribution` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - pybamm.citations.register("Kirk2021") - - def get_fundamental_variables(self): - if self.domain == "Negative": - # distribution variables - c_s_distribution = pybamm.Variable( - "Negative particle concentration distribution", - domain="negative particle", - auxiliary_domains={ - "secondary": "negative particle size", - "tertiary": "negative electrode", - }, - bounds=(0, 1), - ) - R_variable = pybamm.standard_spatial_vars.R_n - - elif self.domain == "Positive": - # distribution variables - c_s_distribution = pybamm.Variable( - "Positive particle concentration distribution", - domain="positive particle", - auxiliary_domains={ - "secondary": "positive particle size", - "tertiary": "positive electrode", - }, - bounds=(0, 1), - ) - R = pybamm.standard_spatial_vars.R_p - - # Distribution variables - variables = self._get_distribution_variables(R) - - # Standard distribution variables (R-dependent) - variables.update( - self._get_standard_concentration_distribution_variables(c_s_distribution) - ) - - # Standard R-averaged variables. Average concentrations using - # the volume-weighted distribution since they are volume-based - # quantities. Necessary for output variables "Total lithium in - # negative electrode [mol]", etc, to be calculated correctly - f_v_dist = variables[ - self.domain + " volume-weighted particle-size distribution" - ] - c_s = pybamm.Integral(f_v_dist * c_s_distribution, R_variable) - c_s_xav = pybamm.x_average(c_s) - variables.update(self._get_standard_concentration_variables(c_s, c_s_xav)) - - return variables - - def get_coupled_variables(self, variables): - c_s_distribution = variables[ - self.domain + " particle concentration distribution" - ] - R_spatial_variable = variables[self.domain + " particle sizes"] - R = pybamm.PrimaryBroadcast( - R_spatial_variable, [self.domain.lower() + " particle"] - ) - T_k = variables[self.domain + " electrode temperature"] - - # Variables can currently only have 3 domains, so remove "current collector" - # from T_k. If T_k was broadcast to "electrode", take orphan, average - # over "current collector", then broadcast to "particle", "particle-size" - # and "electrode" - if isinstance(T_k, pybamm.Broadcast): - T_k = pybamm.yz_average(T_k.orphans[0]) - T_k = pybamm.FullBroadcast( - T_k, self.domain.lower() + " particle", - { - "secondary": self.domain.lower() + " particle size", - "tertiary": self.domain.lower() + " electrode" - } - ) - else: - # broadcast to "particle size" domain then again into "particle" - T_k = pybamm.PrimaryBroadcast( - T_k, - [self.domain.lower() + " particle size"], - ) - T_k = pybamm.PrimaryBroadcast( - T_k, [self.domain.lower() + " particle"], - ) - - if self.domain == "Negative": - N_s_distribution = ( - -self.param.D_n(c_s_distribution, T_k) - * pybamm.grad(c_s_distribution) - / R - ) - f_a_dist = self.param.f_a_dist_n(R_spatial_variable) - - elif self.domain == "Positive": - N_s_distribution = ( - -self.param.D_p(c_s_distribution, T_k) - * pybamm.grad(c_s_distribution) - / R - ) - f_a_dist = self.param.f_a_dist_p(R_spatial_variable) - - # Standard R-averaged flux variables - # Use R_spatial_variable, since "R" is a broadcast - N_s = pybamm.Integral(f_a_dist * N_s_distribution, R_spatial_variable) - variables.update(self._get_standard_flux_variables(N_s, N_s)) - - # Standard distribution flux variables (R-dependent) - variables.update( - {self.domain + " particle flux distribution": N_s_distribution} - ) - - variables.update(self._get_total_concentration_variables(variables)) - return variables - - def set_rhs(self, variables): - c_s_distribution = variables[ - self.domain + " particle concentration distribution" - ] - - N_s_distribution = variables[self.domain + " particle flux distribution"] - - R_spatial_variable = variables[self.domain + " particle sizes"] - R = pybamm.PrimaryBroadcast( - R_spatial_variable, [self.domain.lower() + " particle"] - ) - - if self.domain == "Negative": - self.rhs = { - c_s_distribution: -(1 / self.param.C_n) - * pybamm.div(N_s_distribution) - / R - } - elif self.domain == "Positive": - self.rhs = { - c_s_distribution: -(1 / self.param.C_p) - * pybamm.div(N_s_distribution) - / R - } - - def set_boundary_conditions(self, variables): - # Extract variables - c_s_distribution = variables[ - self.domain + " particle concentration distribution" - ] - c_s_surf_distribution = variables[ - self.domain + " particle surface concentration distribution" - ] - j_distribution = variables[ - self.domain + " electrode interfacial current density distribution" - ] - R_variable = variables[self.domain + " particle size"] - - # Extract T and broadcast to particle size domain - T_k = variables[self.domain + " electrode temperature"] - T_k = pybamm.PrimaryBroadcast( - T_k, [self.domain.lower() + " particle size"] - ) - - # Set surface Neumann boundary values - if self.domain == "Negative": - rbc = ( - -self.param.C_n - * R_variable - * j_distribution - / self.param.a_R_n - / self.param.D_n(c_s_surf_distribution, T_k) - ) - - elif self.domain == "Positive": - rbc = ( - -self.param.C_p - * R_variable - * j_distribution - / self.param.a_R_p - / self.param.gamma_p - / self.param.D_p(c_s_surf_distribution, T_k) - ) - - self.boundary_conditions = { - c_s_distribution: { - "left": (pybamm.Scalar(0), "Neumann"), - "right": (rbc, "Neumann"), - } - } - - def set_initial_conditions(self, variables): - c_s_distribution = variables[ - self.domain + " particle concentration distribution" - ] - - if self.domain == "Negative": - # Broadcast x_n to particle-size then into the particles - x_n = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_n, "negative particle size" - ) - x_n = pybamm.PrimaryBroadcast(x_n, "negative particle") - c_init = self.param.c_n_init(x_n) - - elif self.domain == "Positive": - # Broadcast x_n to particle-size then into the particles - x_p = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.x_p, "positive particle size" - ) - x_p = pybamm.PrimaryBroadcast(x_p, "positive particle") - c_init = self.param.c_p_init(x_p) - - self.initial_conditions = {c_s_distribution: c_init} - - def set_events(self, variables): - c_s_surf_distribution = variables[ - self.domain + " particle surface concentration distribution" - ] - tol = 1e-5 - - self.events.append( - pybamm.Event( - "Minumum " + self.domain.lower() + " particle surface concentration", - pybamm.min(c_s_surf_distribution) - tol, - pybamm.EventType.TERMINATION, - ) - ) - - self.events.append( - pybamm.Event( - "Maximum " + self.domain.lower() + " particle surface concentration", - (1 - tol) - pybamm.max(c_s_surf_distribution), - pybamm.EventType.TERMINATION, - ) - ) From 532d47a624ef2043a7fbdbfd16458b015e86489b Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 16:33:09 +0100 Subject: [PATCH 46/67] revert DFN to develop --- .../full_battery_models/lithium_ion/dfn.py | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index 1bea5a821c..d760793df5 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -85,38 +85,20 @@ def set_particle_submodel(self): [particle_left, "Negative"], [particle_right, "Positive"], ]: - if self.options["particle size"] == "single": - if particle_side == "Fickian diffusion": - self.submodels[ - domain.lower() + " particle" - ] = pybamm.particle.FickianManyParticles(self.param, domain) - elif particle_side in [ - "uniform profile", - "quadratic profile", - "quartic profile", - ]: - self.submodels[ - domain.lower() + " particle" - ] = pybamm.particle.PolynomialManyParticles( - self.param, domain, particle_side - ) - # remove when merging - elif self.options["particle size"] == "distribution": - if particle_side == "Fickian diffusion": - raise pybamm.OptionError( - "Fickian diffusion not yet compatible with" - + " particle size distributions." - ) - elif particle_side in [ - "uniform profile", - "quadratic profile", - "quartic profile", - ]: - self.submodels[ - domain.lower() + " particle" - ] = pybamm.particle.FastManySizeDistributions( - self.param, domain - ) + if particle_side == "Fickian diffusion": + self.submodels[ + domain.lower() + " particle" + ] = pybamm.particle.FickianManyParticles(self.param, domain) + elif particle_side in [ + "uniform profile", + "quadratic profile", + "quartic profile", + ]: + self.submodels[ + domain.lower() + " particle" + ] = pybamm.particle.PolynomialManyParticles( + self.param, domain, particle_side + ) def set_solid_submodel(self): From b3c12eb2c6a390167c6b3ea7bafb285ad578300a Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 17:32:41 +0100 Subject: [PATCH 47/67] remove size distributions from Marquis param set --- ...te_lognormal_particle_size_distribution.py | 30 ------------------- ...o2_lognormal_particle_size_distribution.py | 30 ------------------- 2 files changed, 60 deletions(-) delete mode 100644 pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py delete mode 100644 pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py b/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py deleted file mode 100644 index b82cb8d0af..0000000000 --- a/pybamm/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/graphite_lognormal_particle_size_distribution.py +++ /dev/null @@ -1,30 +0,0 @@ -import pybamm -import numpy as np - - -def graphite_lognormal_particle_size_distribution(R): - """ - A lognormal particle-size distribution as a function of particle radius R. The mean - of the distribution is equal to the "Partice radius [m]" from the parameter set, - and the standard deviation is 0.3 times the mean. - - Parameters - ---------- - R : :class:`pybamm.Symbol` - Particle radius [m] - - """ - # Mean radius (dimensional) - R_av = 1E-5 - - # Standard deviation (dimensional) - sd = R_av * 0.3 - - # calculate usual lognormal parameters - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / (R) - ) diff --git a/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py b/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py deleted file mode 100644 index 17fe9df13e..0000000000 --- a/pybamm/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_lognormal_particle_size_distribution.py +++ /dev/null @@ -1,30 +0,0 @@ -import pybamm -import numpy as np - - -def lico2_lognormal_particle_size_distribution(R): - """ - A lognormal particle-size distribution as a function of particle radius R. The mean - of the distribution is equal to the "Partice radius [m]" from the parameter set, - and the standard deviation is 0.3 times the mean. - - Parameters - ---------- - R : :class:`pybamm.Symbol` - Particle radius [m] - - """ - # Mean radius (dimensional) - R_av = 1E-5 - - # Standard deviation (dimensional) - sd = R_av * 0.3 - - # calculate usual lognormal parameters - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / (R) - ) From f93fa09a9da50843fdfaf8d0e888ec6f1da6b97b Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 30 Jun 2021 17:35:54 +0100 Subject: [PATCH 48/67] remove test for 'FastManyDistributions' submodel --- .../test_fast_many_distributions.py | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py diff --git a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py b/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py deleted file mode 100644 index 2552dbe4ad..0000000000 --- a/tests/unit/test_models/test_submodels/test_particle/test_size_distribution/test_fast_many_distributions.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# Test many size distributions of particles with internal uniform profile -# - -import pybamm -import tests -import unittest - - -class TestManySizeDistributions(unittest.TestCase): - def test_public_functions(self): - param = pybamm.LithiumIonParameters() - - a_n = pybamm.FullBroadcast( - pybamm.Scalar(0), "negative electrode", {"secondary": "current collector"} - ) - a_p = pybamm.FullBroadcast( - pybamm.Scalar(0), "positive electrode", {"secondary": "current collector"} - ) - - variables = { - "Negative electrode interfacial current density distribution": a_n, - "Negative electrode temperature": a_n, - "Negative electrode active material volume fraction": a_n, - "Negative electrode surface area to volume ratio": a_n, - "Negative particle radius": a_n, - } - - submodel = pybamm.particle.FastManySizeDistributions( - param, "Negative" - ) - std_tests = tests.StandardSubModelTests(submodel, variables) - std_tests.test_all() - - variables = { - "Positive electrode interfacial current density distribution": a_p, - "Positive electrode temperature": a_p, - "Positive electrode active material volume fraction": a_p, - "Positive electrode surface area to volume ratio": a_p, - "Positive particle radius": a_p, - } - - submodel = pybamm.particle.FastManySizeDistributions( - param, "Positive" - ) - 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 f00d86dc02561c7c6c9fde78eb4d56360fa9d0c1 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Mon, 5 Jul 2021 13:07:55 +0100 Subject: [PATCH 49/67] change name of R_average --- docs/source/expression_tree/unary_operator.rst | 2 +- pybamm/expression_tree/unary_operators.py | 6 ++++-- .../active_material/base_active_material.py | 4 ++-- .../submodels/interface/base_interface.py | 10 +++++----- .../fast_single_distribution.py | 2 +- .../fickian_single_distribution.py | 2 +- .../test_unary_operators.py | 17 +++++++++-------- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/source/expression_tree/unary_operator.rst b/docs/source/expression_tree/unary_operator.rst index 1a96e5d29a..ad5bb0a48f 100644 --- a/docs/source/expression_tree/unary_operator.rst +++ b/docs/source/expression_tree/unary_operator.rst @@ -81,7 +81,7 @@ Unary Operators .. autofunction:: pybamm.r_average -.. autofunction:: pybamm.R_average +.. autofunction:: pybamm.size_average .. autofunction:: pybamm.z_average diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 1ba6ad7905..632ab249fd 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1399,7 +1399,7 @@ def r_average(symbol): return Integral(symbol, r) / Integral(v, r) -def R_average(symbol, param): +def size_average(symbol, param): """convenience function for averaging over particle size R using the area-weighted particle-size distribution. @@ -1417,7 +1417,9 @@ def R_average(symbol, param): """ # Can't take average if the symbol evaluates on edges if symbol.evaluates_on_edges("primary"): - raise ValueError("Can't take the R-average of a symbol that evaluates on edges") + raise ValueError( + """Can't take the size-average of a symbol that evaluates on edges""" + ) # If symbol doesn't have a domain, or doesn't have "negative particle size" # or "positive particle size" as a domain, it's average value is itself diff --git a/pybamm/models/submodels/active_material/base_active_material.py b/pybamm/models/submodels/active_material/base_active_material.py index 610ab38c31..8a7714b477 100644 --- a/pybamm/models/submodels/active_material/base_active_material.py +++ b/pybamm/models/submodels/active_material/base_active_material.py @@ -80,7 +80,7 @@ def _get_standard_active_material_variables(self, eps_solid): R_dim = self.param.R_n_dimensional(x * self.param.L_x) elif self.options["particle size"] == "distribution": R_n = pybamm.standard_spatial_vars.R_n - R = pybamm.R_average(R_n, self.param) + R = pybamm.size_average(R_n, self.param) R_dim = R * self.param.R_n_typ a_typ = self.param.a_n_typ elif self.domain == "Positive": @@ -90,7 +90,7 @@ def _get_standard_active_material_variables(self, eps_solid): R_dim = self.param.R_p_dimensional(x * self.param.L_x) elif self.options["particle size"] == "distribution": R_p = pybamm.standard_spatial_vars.R_p - R = pybamm.R_average(R_p, self.param) + R = pybamm.size_average(R_p, self.param) R_dim = R * self.param.R_p_typ a_typ = self.param.a_p_typ diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 4996f0f1dc..27e1864e9b 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -448,7 +448,7 @@ def _get_standard_exchange_current_variables(self, j0): # output exchange current density if j0.domain == [self.domain.lower() + " particle size"]: # R-average - j0 = pybamm.R_average(j0, self.param) + j0 = pybamm.size_average(j0, self.param) # X-average, and broadcast if necessary if j0.domain == []: @@ -535,7 +535,7 @@ def _get_standard_overpotential_variables(self, eta_r): # output reaction overpotential if eta_r.domain == [self.domain.lower() + " particle size"]: # R-average - eta_r = pybamm.R_average(eta_r, self.param) + eta_r = pybamm.size_average(eta_r, self.param) # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) @@ -650,7 +650,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # output open circuit potential if ocp.domain == [self.domain.lower() + " particle size"]: # R-average - ocp = pybamm.R_average(ocp, self.param) + ocp = pybamm.size_average(ocp, self.param) # X-average, and broadcast if necessary if ocp.domain == []: @@ -668,7 +668,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): # output entropic change if dUdT.domain == [self.domain.lower() + " particle size"]: # R-average - dUdT = pybamm.R_average(dUdT, self.param) + dUdT = pybamm.size_average(dUdT, self.param) dUdT_av = pybamm.x_average(dUdT) @@ -731,7 +731,7 @@ def _get_PSD_current_densities(self, j0, ne, eta_r, T): j_distribution = self._get_kinetics(j0, ne, eta_r, T) # R-average - j = pybamm.R_average(j_distribution, self.param) + j = pybamm.size_average(j_distribution, self.param) return j, j_distribution def _get_standard_PSD_interfacial_current_variables(self, j_distribution): diff --git a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py index 4750fd9228..f232a171e4 100644 --- a/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fast_single_distribution.py @@ -175,7 +175,7 @@ def set_events(self, variables): self.events.append( pybamm.Event( - "Minumum " + self.domain.lower() + " particle surface concentration", + "Minimum " + self.domain.lower() + " particle surface concentration", pybamm.min(c_s_surf_xav_distribution) - tol, pybamm.EventType.TERMINATION, ) diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index 94382c982c..a0f45fccaf 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -257,7 +257,7 @@ def set_events(self, variables): self.events.append( pybamm.Event( - "Minumum " + self.domain.lower() + " particle surface concentration", + "Minimum " + self.domain.lower() + " particle surface concentration", pybamm.min(c_s_surf_xav_distribution) - tol, pybamm.EventType.TERMINATION, ) diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 837ec2ae3e..034d3e04c5 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -627,12 +627,12 @@ def test_x_average(self): self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[1].id, l_p.id) - def test_R_average(self): + def test_size_average(self): param = pybamm.LithiumIonParameters() # no domain a = pybamm.Scalar(1) - average_a = pybamm.R_average(a, param) + average_a = pybamm.size_average(a, param) self.assertEqual(average_a.id, a.id) b = pybamm.FullBroadcast( @@ -644,18 +644,18 @@ def test_R_average(self): } ) # no "particle size" domain - average_b = pybamm.R_average(b, param) + average_b = pybamm.size_average(b, param) self.assertEqual(average_b.id, b.id) # primary or secondary broadcast to "particle size" domain - average_a = pybamm.R_average( + average_a = pybamm.size_average( pybamm.PrimaryBroadcast(a, "negative particle size"), param ) self.assertEqual(average_a.evaluate(), np.array([1])) a = pybamm.Symbol("a", domain="negative particle") - average_a = pybamm.R_average( + average_a = pybamm.size_average( pybamm.SecondaryBroadcast(a, "negative particle size"), param ) @@ -664,7 +664,7 @@ def test_R_average(self): for domain in [["negative particle size"], ["positive particle size"]]: a = pybamm.Symbol("a", domain=domain) R = pybamm.SpatialVariable("R", domain) - av_a = pybamm.R_average(a, param) + av_a = pybamm.size_average(a, param) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertIsInstance(av_a.children[1], pybamm.Integral) @@ -675,9 +675,10 @@ def test_R_average(self): # R-average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") with self.assertRaisesRegex( - ValueError, "Can't take the R-average of a symbol that evaluates on edges" + ValueError, + """Can't take the size-average of a symbol that evaluates on edges""" ): - pybamm.R_average(symbol_on_edges, param) + pybamm.size_average(symbol_on_edges, param) def test_r_average(self): a = pybamm.Scalar(1) From e081d08b0c94865fa97199d8789547ffdd2f7181 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Mon, 5 Jul 2021 13:08:18 +0100 Subject: [PATCH 50/67] fix exchange current density tests --- .../test_submodels/test_interface/test_lithium_ion.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py b/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py index 60c4a875b2..5e3b0a2194 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py @@ -28,6 +28,7 @@ def setUp(self): "Negative electrode temperature": 0, "Positive electrode temperature": 0, } + self.options = {"particle size": "single"} def tearDown(self): del self.variables @@ -38,8 +39,10 @@ def tearDown(self): def test_creation_lithium_ion(self): param = pybamm.LithiumIonParameters() model_n = pybamm.interface.BaseInterface(param, "Negative", "lithium-ion main") + model_n.options = self.options j0_n = model_n._get_exchange_current_density(self.variables) model_p = pybamm.interface.BaseInterface(param, "Positive", "lithium-ion main") + model_p.options = self.options j0_p = model_p._get_exchange_current_density(self.variables) self.assertEqual(j0_n.domain, ["negative electrode"]) self.assertEqual(j0_p.domain, ["positive electrode"]) @@ -47,8 +50,10 @@ def test_creation_lithium_ion(self): def test_set_parameters_lithium_ion(self): param = pybamm.LithiumIonParameters() model_n = pybamm.interface.BaseInterface(param, "Negative", "lithium-ion main") + model_n.options = self.options j0_n = model_n._get_exchange_current_density(self.variables) model_p = pybamm.interface.BaseInterface(param, "Positive", "lithium-ion main") + model_p.options = self.options j0_p = model_p._get_exchange_current_density(self.variables) # Process parameters parameter_values = pybamm.lithium_ion.BaseModel().default_parameter_values @@ -63,8 +68,10 @@ def test_set_parameters_lithium_ion(self): def test_discretisation_lithium_ion(self): param = pybamm.LithiumIonParameters() model_n = pybamm.interface.BaseInterface(param, "Negative", "lithium-ion main") + model_n.options = self.options j0_n = model_n._get_exchange_current_density(self.variables) model_p = pybamm.interface.BaseInterface(param, "Positive", "lithium-ion main") + model_p.options = self.options j0_p = model_p._get_exchange_current_density(self.variables) # Process parameters and discretise parameter_values = pybamm.lithium_ion.BaseModel().default_parameter_values From c955d72fe3b8f03ec877010030597067898fcf00 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 6 Jul 2021 11:56:22 +0100 Subject: [PATCH 51/67] add function get_size_distribution_parameters --- examples/notebooks/models/MPM.ipynb | 55 ++---- pybamm/__init__.py | 1 + .../full_battery_models/lithium_ion/mpm.py | 175 +----------------- .../size_distribution_parameters.py | 124 +++++++++++++ .../test_lithium_ion/test_compare_outputs.py | 52 +----- .../test_lithium_ion/test_mpm.py | 2 +- 6 files changed, 160 insertions(+), 249 deletions(-) create mode 100644 pybamm/parameters/size_distribution_parameters.py diff --git a/examples/notebooks/models/MPM.ipynb b/examples/notebooks/models/MPM.ipynb index ab8d107e9c..653c1078ff 100644 --- a/examples/notebooks/models/MPM.ipynb +++ b/examples/notebooks/models/MPM.ipynb @@ -298,7 +298,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "fb2cf1a877d243cea4aa5eae1144b78b", + "model_id": "b66ea72826fe48e69565e67441ebc889", "version_major": 2, "version_minor": 0 }, @@ -312,7 +312,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 7, @@ -448,24 +448,6 @@ "metadata": {}, "outputs": [], "source": [ - "# Define a lognormal distribution\n", - "def lognormal(R, R_av, sd):\n", - " '''\n", - " A lognormal distribution with arguments\n", - " R : particle radius\n", - " R_av: mean particle radius\n", - " sd : standard deviation\n", - " '''\n", - " # calculate usual lognormal parameters\n", - " mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2))\n", - " sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2))\n", - " return (\n", - " pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2))\n", - " / pybamm.sqrt(2 * np.pi * sigma_ln ** 2)\n", - " / R\n", - " )\n", - "\n", - "\n", "# Parameter set (no distribution parameters by default)\n", "params = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019)\n", "\n", @@ -483,15 +465,16 @@ "R_max_n = 2 * R_a_n_dim\n", "R_max_p = 3 * R_a_p_dim\n", "\n", - "# Set the area-weighted particle-size distributions\n", - "# Note: the only argument must be the particle size R\n", + "# Set the area-weighted particle-size distributions.\n", + "# Choose a lognormal (but any pybamm function could be used)\n", "def f_a_dist_n_dim(R):\n", - " return lognormal(R, R_a_n_dim, sd_a_n_dim)\n", + " return pybamm.lognormal(R, R_a_n_dim, sd_a_n_dim)\n", "\n", "\n", "def f_a_dist_p_dim(R):\n", - " return lognormal(R, R_a_p_dim, sd_a_p_dim)\n", - "\n" + " return pybamm.lognormal(R, R_a_p_dim, sd_a_p_dim)\n", + "\n", + "# Note: the only argument must be the particle size R\n" ] }, { @@ -522,7 +505,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5dff78a1e2bc4a1884d5811ff443897c", + "model_id": "8b545ec433484ec1b738983bb0d90b80", "version_major": 2, "version_minor": 0 }, @@ -536,7 +519,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 12, @@ -621,7 +604,7 @@ "\n", "# Set the area-weighted particle-size distribution\n", "def f_a_dist_p_dim(R):\n", - " return lognormal(R, R_a_p_dim, sd_a_p_dim)\n", + " return pybamm.lognormal(R, R_a_p_dim, sd_a_p_dim)\n", "\n", "# input to param dictionary\n", "distribution_params = {\n", @@ -641,7 +624,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "1d8c9493b65344a580dbe0cd7fef4081", + "model_id": "9e0887e23c884591bdd97caac3e670d5", "version_major": 2, "version_minor": 0 }, @@ -655,7 +638,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 15, @@ -766,7 +749,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "30e5982a7e1345d7912e3aa131fa6ff7", + "model_id": "ca33360fe25f423fbae0fe20bad989e1", "version_major": 2, "version_minor": 0 }, @@ -780,7 +763,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 18, @@ -830,7 +813,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6938c3cab8f14709985552080e01355e", + "model_id": "4cfa09f7cdac41cfac0c38ab4be1cf38", "version_major": 2, "version_minor": 0 }, @@ -844,7 +827,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 19, @@ -884,7 +867,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0ff76ec97d994aed8415b4088e84b925", + "model_id": "462351065ca445868719ac5a85a66cf7", "version_major": 2, "version_minor": 0 }, @@ -898,7 +881,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 20, diff --git a/pybamm/__init__.py b/pybamm/__init__.py index e5f476195a..c01fd49938 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -173,6 +173,7 @@ def version(formatted=False): from .parameters.thermal_parameters import thermal_parameters, ThermalParameters from .parameters.lithium_ion_parameters import LithiumIonParameters from .parameters.lead_acid_parameters import LeadAcidParameters +from .parameters.size_distribution_parameters import * from .parameters import parameter_sets # diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index fa7dce9a4e..69e97f9634 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -2,10 +2,10 @@ # Many-Particle Model (MPM) # import pybamm -from .base_lithium_ion_model import BaseModel +from .spm import SPM -class MPM(BaseModel): +class MPM(SPM): """Many-Particle Model (MPM) of a lithium-ion battery with particle-size distributions for each electrode, from [1]_. @@ -28,7 +28,7 @@ class MPM(BaseModel): distributions”. In: arXiv preprint arXiv:2006.12208 (2020). - **Extends:** :class:`pybamm.lithium_ion.BaseModel` + **Extends:** :class:`pybamm.lithium_ion.SPM` """ def __init__( @@ -43,12 +43,12 @@ def __init__( else: options["particle size"] = "distribution" options["surface form"] = "algebraic" + super(SPM, self).__init__(options, name) + # For degradation models we use the "x-average" form since this is a # reduced-order model with uniform current density in the electrodes self.x_average = True - super().__init__(options, name) - # Set submodels self.set_external_circuit_submodel() self.set_porosity_submodel() self.set_crack_submodel() @@ -73,25 +73,6 @@ def __init__( pybamm.citations.register("Kirk2020") pybamm.citations.register("Kirk2021") - def set_convection_submodel(self): - - self.submodels[ - "through-cell convection" - ] = pybamm.convection.through_cell.NoConvection(self.param) - self.submodels[ - "transverse convection" - ] = pybamm.convection.transverse.NoConvection(self.param) - - def set_interfacial_submodel(self): - - self.submodels["negative interface"] = pybamm.interface.ButlerVolmer( - self.param, "Negative", "lithium-ion main", self.options - ) - - self.submodels["positive interface"] = pybamm.interface.ButlerVolmer( - self.param, "Positive", "lithium-ion main", self.options - ) - def set_particle_submodel(self): if self.options["particle size"] != "distribution": @@ -118,154 +99,10 @@ def set_particle_submodel(self): self.submodels["negative particle"] = submod_n self.submodels["positive particle"] = submod_p - def set_negative_electrode_submodel(self): - - self.submodels[ - "negative electrode potential" - ] = pybamm.electrode.ohm.LeadingOrder(self.param, "Negative") - - def set_positive_electrode_submodel(self): - - self.submodels[ - "positive electrode potential" - ] = pybamm.electrode.ohm.LeadingOrder(self.param, "Positive") - - def set_electrolyte_submodel(self): - - surf_form = pybamm.electrolyte_conductivity.surface_potential_form - - if self.options["electrolyte conductivity"] not in ["default", "leading order"]: - raise pybamm.OptionError( - "electrolyte conductivity '{}' not suitable for MPM".format( - self.options["electrolyte conductivity"] - ) - ) - if self.options["surface form"] != "algebraic": - raise pybamm.OptionError( - "surface form must be 'algebraic' not '{}' for MPM".format( - self.options["electrolyte conductivity"] - ) - ) - else: - for domain in ["Negative", "Separator", "Positive"]: - self.submodels[ - "leading-order " + domain.lower() + " electrolyte conductivity" - ] = surf_form.LeadingOrderAlgebraic(self.param, domain) - - self.submodels[ - "electrolyte diffusion" - ] = pybamm.electrolyte_diffusion.ConstantConcentration(self.param) - - def set_thermal_submodel(self): - - if self.options["thermal"] == "isothermal": - thermal_submodel = pybamm.thermal.isothermal.Isothermal(self.param) - - elif self.options["thermal"] == "lumped": - thermal_submodel = pybamm.thermal.Lumped( - self.param, - cc_dimension=self.options["dimensionality"], - geometry=self.options["cell geometry"], - ) - - elif self.options["thermal"] == "x-lumped": - if self.options["dimensionality"] == 0: - # With 0D current collectors x-lumped is equivalent to lumped pouch - thermal_submodel = pybamm.thermal.Lumped(self.param, geometry="pouch") - elif self.options["dimensionality"] == 1: - thermal_submodel = pybamm.thermal.pouch_cell.CurrentCollector1D( - self.param - ) - elif self.options["dimensionality"] == 2: - thermal_submodel = pybamm.thermal.pouch_cell.CurrentCollector2D( - self.param - ) - - elif self.options["thermal"] == "x-full": - raise NotImplementedError( - """X-full thermal submodels do - not yet support particle-size distributions.""" - ) - - self.submodels["thermal"] = thermal_submodel - - def set_sei_submodel(self): - - # negative electrode SEI - self.submodels["negative sei"] = pybamm.sei.NoSEI(self.param, "Negative") - - # positive electrode - self.submodels["positive sei"] = pybamm.sei.NoSEI(self.param, "Positive") - @property def default_parameter_values(self): default_params = super().default_parameter_values - - # The mean particle radii for each electrode, taken to be the - # "Negative particle radius [m]" and "Positive particle radius [m]" - # provided in the parameter set. These will be the means of the - # (area-weighted) particle-size distributions f_a_dist_n_dim, - # f_a_dist_p_dim, provided below. - R_n_dim = default_params["Negative particle radius [m]"] - R_p_dim = default_params["Positive particle radius [m]"] - - # Standard deviations (dimensionless) - sd_a_n = 0.3 - sd_a_p = 0.3 - - # Minimum radius in the particle-size distributions (dimensionless). - R_min_n = 0 - R_min_p = 0 - - # Max radius in the particle-size distributions (dimensionless). - # 5 standard deviations above the mean - R_max_n = 1 + sd_a_n * 5 - R_max_p = 1 + sd_a_p * 5 - - # Define lognormal distribution - def lognormal_distribution(R, R_av, sd): - ''' - A lognormal distribution with arguments - R : particle radius - R_av: mean particle radius - sd : standard deviation - (Inputs can be dimensional or dimensionless) - ''' - import numpy as np - - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / (R) - ) - - # Set the dimensional (area-weighted) particle-size distributions - def f_a_dist_n_dim(R): - return lognormal_distribution(R, R_n_dim, sd_a_n * R_n_dim) - - def f_a_dist_p_dim(R): - return lognormal_distribution(R, R_p_dim, sd_a_p * R_p_dim) - - # Append to default parameters - default_params.update( - { - "Negative area-weighted particle-size " - + "standard deviation [m]": sd_a_n * R_n_dim, - "Positive area-weighted particle-size " - + "standard deviation [m]": sd_a_p * R_p_dim, - "Negative minimum particle radius [m]": R_min_n * R_n_dim, - "Positive minimum particle radius [m]": R_min_p * R_p_dim, - "Negative maximum particle radius [m]": R_max_n * R_n_dim, - "Positive maximum particle radius [m]": R_max_p * R_p_dim, - "Negative area-weighted " - + "particle-size distribution [m-1]": f_a_dist_n_dim, - "Positive area-weighted " - + "particle-size distribution [m-1]": f_a_dist_p_dim, - }, - check_already_exists=False, - ) + default_params = pybamm.get_size_distribution_parameters(default_params) return default_params diff --git a/pybamm/parameters/size_distribution_parameters.py b/pybamm/parameters/size_distribution_parameters.py new file mode 100644 index 0000000000..3c57770ff9 --- /dev/null +++ b/pybamm/parameters/size_distribution_parameters.py @@ -0,0 +1,124 @@ +""" +Adding particle-size distribution parameter values to a parameter set +""" + + +import pybamm +import numpy as np + + +def get_size_distribution_parameters( + param, + R_n_av=None, + R_p_av=None, + sd_n=None, + sd_p=None, + R_min_n=None, + R_min_p=None, + R_max_n=None, + R_max_p=None, +): + """ + A convenience method to add standard area-weighted particle-size distribution + parameter values to a parameter set. A lognormal distribution is assumed for + each electrode, with mean set equal to the particle radius parameter in the + set (default) or a custom value. The standard deviations and min/max radii + are specified relative (i.e. scaled by) the mean radius for convenience. + Only the dimensional values are output from this method. + + Parameters + ---------- + param : :class:`pybamm.ParameterValues` + The parameter values to add the distribution parameters to. + R_n_av : float (optional) + The area-weighted mean particle radius (dimensional) of the negative electrode. + Default is the value "Negative particle radius [m]" from param. + R_p_av : float (optional) + The area-weighted mean particle radius (dimensional) of the positive electrode. + Default is the value "Positive particle radius [m]" from param. + sd_n : float (optional) + The area-weighted standard deviation, scaled by the mean radius R_n_av, + hence dimensionless. Default is 0.3. + sd_p : float (optional) + The area-weighted standard deviation, scaled by the mean radius R_p_av, + hence dimensionless. Default is 0.3. + R_min_n : float (optional) + Minimum radius in negative electrode, scaled by the mean radius R_n_av. + Default is 0 or 5 standard deviations below the mean (if positive). + R_min_p : float (optional) + Minimum radius in positive electrode, scaled by the mean radius R_p_av. + Default is 0 or 5 standard deviations below the mean (if positive). + R_max_n : float (optional) + Maximum radius in negative electrode, scaled by the mean radius R_n_av. + Default is 5 standard deviations above the mean. + R_max_p : float (optional) + Maximum radius in positive electrode, scaled by the mean radius R_p_av. + Default is 5 standard deviations above the mean. + """ + + # Radii from given parameter set + R_n_typ = param["Negative particle radius [m]"] + R_p_typ = param["Positive particle radius [m]"] + + # Set the mean particle radii for each electrode + R_n_av = R_n_av or R_n_typ + R_p_av = R_p_av or R_p_typ + + # Standard deviations (scaled by the mean radius) + sd_n = sd_n or 0.3 + sd_p = sd_p or 0.3 + + # Minimum radii + R_min_n = R_min_n or np.max([0, 1 - sd_n * 5]) + R_min_p = R_min_p or np.max([0, 1 - sd_p * 5]) + + # Max radii + R_max_n = R_max_n or (1 + sd_n * 5) + R_max_p = R_max_p or (1 + sd_p * 5) + + # Area-weighted particle-size distributions + def f_a_dist_n_dim(R): + return lognormal(R, R_n_av, sd_n * R_n_av) + + def f_a_dist_p_dim(R): + return lognormal(R, R_p_av, sd_p * R_p_av) + + param.update( + { + "Negative area-weighted mean particle radius [m]": R_n_av, + "Positive area-weighted mean particle radius [m]": R_p_av, + "Negative area-weighted particle-size " + + "standard deviation [m]": sd_n * R_n_av, + "Positive area-weighted particle-size " + + "standard deviation [m]": sd_p * R_p_av, + "Negative minimum particle radius [m]": R_min_n * R_n_av, + "Positive minimum particle radius [m]": R_min_p * R_p_av, + "Negative maximum particle radius [m]": R_max_n * R_n_av, + "Positive maximum particle radius [m]": R_max_p * R_p_av, + "Negative area-weighted " + + "particle-size distribution [m-1]": f_a_dist_n_dim, + "Positive area-weighted " + + "particle-size distribution [m-1]": f_a_dist_p_dim, + }, + check_already_exists=False, + ) + return param + + +def lognormal(x, x_av, sd): + """ + A PyBaMM lognormal distribution for use with particle-size distribution models. + The independent variable is x, range 0 < x < Inf, with mean x_av and standard + deviation sd. Note: if, e.g. X is lognormally distributed, then the mean and + standard deviations used here are of X rather than the normal distribution log(X). + """ + + mu_ln = pybamm.log(x_av ** 2 / pybamm.sqrt(x_av ** 2 + sd ** 2)) + sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / x_av ** 2)) + + out = ( + pybamm.exp(-((pybamm.log(x) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) + / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) + / x + ) + return out diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py index 56ae1aaef2..7989c91833 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py @@ -168,49 +168,15 @@ def test_compare_narrow_size_distribution(self): param = models[0].default_parameter_values - # Set size distribution parameters - R_n_dim = param["Negative particle radius [m]"] - R_p_dim = param["Positive particle radius [m]"] - - # Very small standard deviations - sd_a_n = 0.05 - sd_a_p = 0.05 - - # Min and max radii - R_min_n = 0.8 - R_min_p = 0.8 - R_max_n = 1.2 - R_max_p = 1.2 - - def lognormal(R, R_av, sd): - import numpy as np - - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / R - ) - - def f_a_dist_n_dim(R): - return lognormal(R, R_n_dim, sd_a_n * R_n_dim) - - def f_a_dist_p_dim(R): - return lognormal(R, R_p_dim, sd_a_p * R_p_dim) - - param.update( - { - "Negative minimum particle radius [m]": R_min_n * R_n_dim, - "Positive minimum particle radius [m]": R_min_p * R_p_dim, - "Negative maximum particle radius [m]": R_max_n * R_n_dim, - "Positive maximum particle radius [m]": R_max_p * R_p_dim, - "Negative area-weighted " - + "particle-size distribution [m-1]": f_a_dist_n_dim, - "Positive area-weighted " - + "particle-size distribution [m-1]": f_a_dist_p_dim, - }, - check_already_exists=False, + # Set size distribution parameters (lognormals) + param = pybamm.get_size_distribution_parameters( + param, + sd_n=0.05, # small standard deviations + sd_p=0.05, + R_min_n=0.8, + R_min_p=0.8, + R_max_n=1.2, + R_max_p=1.2, ) # set same mesh for both models diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index b17911cdf6..0c11681351 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -14,7 +14,7 @@ def test_basic_processing(self): model = pybamm.lithium_ion.MPM(options) # use Ecker parameters for nonlinear diffusion param = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) - param = self.set_distribution_params_for_test(param) + param = pybamm.get_size_distribution_parameters(param) modeltest = tests.StandardModelTest(model) modeltest.test_all() From 23fa9130f0c3d4c5f7325b06d17173f98ece0ac5 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 6 Jul 2021 14:25:09 +0100 Subject: [PATCH 52/67] refactor interface distribution variables --- .../submodels/interface/base_interface.py | 218 +++++++++++++----- .../interface/kinetics/base_kinetics.py | 34 ++- 2 files changed, 188 insertions(+), 64 deletions(-) diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 27e1864e9b..fd9aab98c8 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -289,6 +289,10 @@ def _get_standard_interfacial_current_variables(self, j): elif self.domain == "Positive": j_scale = i_typ / (self.param.a_p_typ * L_x) + # Size average. For j variables that depend on particle size, see + # "_get_standard_size_distribution_interfacial_current_variables" + j = pybamm.size_average(j, self.param) + # Average, and broadcast if necessary if j.domain == []: j_av = j @@ -444,11 +448,9 @@ def _get_standard_exchange_current_variables(self, j0): elif self.domain == "Positive": j_scale = i_typ / (self.param.a_p_typ * L_x) - # If j0 depends on particle size R then must R-average to get standard - # output exchange current density - if j0.domain == [self.domain.lower() + " particle size"]: - # R-average - j0 = pybamm.size_average(j0, self.param) + # Size average. For j0 variables that depend on particle size, see + # "_get_standard_size_distribution_exchange_current_variables" + j0 = pybamm.size_average(j0, self.param) # X-average, and broadcast if necessary if j0.domain == []: @@ -531,11 +533,10 @@ def _get_standard_whole_cell_exchange_current_variables(self, variables): def _get_standard_overpotential_variables(self, eta_r): pot_scale = self.param.potential_scale - # If eta_r depends on particle size R then must R-average to get standard - # output reaction overpotential - if eta_r.domain == [self.domain.lower() + " particle size"]: - # R-average - eta_r = pybamm.size_average(eta_r, self.param) + + # Size average. For eta_r variables that depend on particle size, see + # "_get_standard_size_distribution_overpotential_variables" + eta_r = pybamm.size_average(eta_r, self.param) # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) @@ -646,11 +647,9 @@ def _get_standard_ocp_variables(self, ocp, dUdT): The variables dictionary including the open circuit potentials and related standard variables. """ - # If ocp depends on particle size R then must R-average to get standard - # output open circuit potential - if ocp.domain == [self.domain.lower() + " particle size"]: - # R-average - ocp = pybamm.size_average(ocp, self.param) + # Size average. For ocp variables that depend on particle size, see + # "_get_standard_size_distribution_ocp_variables" + ocp = pybamm.size_average(ocp, self.param) # X-average, and broadcast if necessary if ocp.domain == []: @@ -664,11 +663,8 @@ def _get_standard_ocp_variables(self, ocp, dUdT): else: ocp_av = pybamm.x_average(ocp) - # If dUdT depends on particle size R then must R-average to get standard - # output entropic change - if dUdT.domain == [self.domain.lower() + " particle size"]: - # R-average - dUdT = pybamm.size_average(dUdT, self.param) + # Size average + dUdT = pybamm.size_average(dUdT, self.param) dUdT_av = pybamm.x_average(dUdT) @@ -711,42 +707,19 @@ def _get_standard_ocp_variables(self, ocp, dUdT): return variables - def _get_PSD_current_densities(self, j0, ne, eta_r, T): - """ - Calculates current density (j_distribution) that depends on - particle size for "particle-size distribution" models, and - the R-averaged (using area-weighted distribution) current density (j) - """ - # T must have same domains as j0, eta_r, so remove electrode domain from T - # if necessary (only check eta_r, as j0 should already match) - if eta_r.domains["secondary"] != [self.domain.lower() + " electrode"]: - T = pybamm.x_average(T) - - # Broadcast T onto "particle size" domain - T = pybamm.PrimaryBroadcast( - T, [self.domain.lower() + " particle size"] - ) - - # current density that depends on particle size R - j_distribution = self._get_kinetics(j0, ne, eta_r, T) - - # R-average - j = pybamm.size_average(j_distribution, self.param) - return j, j_distribution - - def _get_standard_PSD_interfacial_current_variables(self, j_distribution): + def _get_standard_size_distribution_interfacial_current_variables(self, j): """ Interfacial current density variables that depend on particle size R, relevant if "particle size" option is "distribution". """ # X-average and broadcast if necessary - if j_distribution.domains["secondary"] == [self.domain.lower() + " electrode"]: + if j.domains["secondary"] == [self.domain.lower() + " electrode"]: # x-average - j_xav_distribution = pybamm.x_average(j_distribution) + j_xav = pybamm.x_average(j) else: - j_xav_distribution = j_distribution - j_distribution = pybamm.SecondaryBroadcast( - j_xav_distribution, [self.domain.lower() + " electrode"] + j_xav = j + j = pybamm.SecondaryBroadcast( + j_xav, [self.domain.lower() + " electrode"] ) #j scale @@ -761,23 +734,162 @@ def _get_standard_PSD_interfacial_current_variables(self, j_distribution): self.domain + " electrode" + self.reaction_name - + " interfacial current density distribution": j_distribution, + + " interfacial current density distribution": j, "X-averaged " + self.domain.lower() + " electrode" + self.reaction_name - + " interfacial current density distribution": j_xav_distribution, + + " interfacial current density distribution": j_xav, self.domain + " electrode" + self.reaction_name + " interfacial current density" - + " distribution [A.m-2]": j_scale * j_distribution, + + " distribution [A.m-2]": j_scale * j, "X-averaged " + self.domain.lower() + " electrode" + self.reaction_name + " interfacial current density" - + " distribution [A.m-2]": j_scale * j_xav_distribution, + + " distribution [A.m-2]": j_scale * j_xav, + } + + return variables + + def _get_standard_size_distribution_exchange_current_variables(self, j0): + """ + Exchange current variables that depend on particle size. + """ + i_typ = self.param.i_typ + L_x = self.param.L_x + if self.domain == "Negative": + j_scale = i_typ / (self.param.a_n_typ * L_x) + elif self.domain == "Positive": + j_scale = i_typ / (self.param.a_p_typ * L_x) + + # X-average or broadcast to electrode if necessary + if j0.domains["secondary"] != [self.domain.lower() + " electrode"]: + j0_av = j0 + j0 = pybamm.SecondaryBroadcast(j0, self.domain_for_broadcast) + else: + j0_av = pybamm.x_average(j0) + + variables = { + self.domain + + " electrode" + + self.reaction_name + + " exchange current density distribution": j0, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " exchange current density distribution": j0_av, + self.domain + + " electrode" + + self.reaction_name + + " exchange current density distribution [A.m-2]": j_scale * j0, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " exchange current density distribution [A.m-2]": j_scale * j0_av, + self.domain + + " electrode" + + self.reaction_name + + " exchange current density distribution" + + " per volume [A.m-3]": i_typ / L_x * j0, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " exchange current density distribution" + + " per volume [A.m-3]": i_typ / L_x * j0_av, + } + + return variables + + def _get_standard_size_distribution_overpotential_variables(self, eta_r): + """ + Overpotential variables that depend on particle size. + """ + pot_scale = self.param.potential_scale + + # X-average or broadcast to electrode if necessary + if eta_r.domains["secondary"] != [self.domain.lower() + " electrode"]: + eta_r_av = eta_r + eta_r = pybamm.SecondaryBroadcast(eta_r, self.domain_for_broadcast) + else: + eta_r_av = pybamm.x_average(eta_r) + + domain_reaction = ( + self.domain + " electrode" + self.reaction_name + " reaction overpotential" + ) + + variables = { + domain_reaction: eta_r, + "X-averaged " + domain_reaction.lower() + " distribution": eta_r_av, + domain_reaction + " [V]": eta_r * pot_scale, + "X-averaged " + domain_reaction.lower() + + " distribution [V]": eta_r_av * pot_scale, } return variables + + def _get_standard_size_distribution_ocp_variables(self, ocp, dUdT): + """ + A private function to obtain the open circuit potential and + related standard variables when there is a distribution of particle sizes. + """ + + # X-average or broadcast to electrode if necessary + if ocp.domains["secondary"] != [self.domain.lower() + " electrode"]: + ocp_av = ocp + ocp = pybamm.SecondaryBroadcast(ocp, self.domain_for_broadcast) + else: + ocp_av = pybamm.x_average(ocp) + + if dUdT.domains["secondary"] != [self.domain.lower() + " electrode"]: + dUdT_av = dUdT + dUdT = pybamm.SecondaryBroadcast(dUdT, self.domain_for_broadcast) + else: + dUdT_av = pybamm.x_average(dUdT) + + if self.domain == "Negative": + ocp_dim = self.param.U_n_ref + self.param.potential_scale * ocp + ocp_av_dim = self.param.U_n_ref + self.param.potential_scale * ocp_av + elif self.domain == "Positive": + ocp_dim = self.param.U_p_ref + self.param.potential_scale * ocp + ocp_av_dim = self.param.U_p_ref + self.param.potential_scale * ocp_av + + variables = { + self.domain + + " electrode" + + self.reaction_name + + " open circuit potential distribution": ocp, + self.domain + + " electrode" + + self.reaction_name + + " open circuit potential distribution [V]": ocp_dim, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " open circuit potential distribution": ocp_av, + "X-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " open circuit potential distribution [V]": ocp_av_dim, + } + if self.reaction_name == "": + variables.update( + { + self.domain + " electrode entropic change" + + " (size-dependent)": dUdT, + "X-averaged " + + self.domain.lower() + + " electrode entropic change" + + " (size-dependent)": dUdT_av, + } + ) + + return variables diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index 0fb23822e2..af509676f8 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -112,25 +112,37 @@ def get_coupled_variables(self, variables): # 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"] + elif j0.domain == [self.domain.lower() + " particle size"]: + if j0.domains["secondary"] != [self.domain.lower() + " electrode"]: + T = variables["X-averaged cell temperature"] + else: + T = variables[self.domain + " electrode temperature"] + + # Broadcast T onto "particle size" domain + T = pybamm.PrimaryBroadcast(T, [self.domain.lower() + " particle size"]) else: T = variables[self.domain + " electrode temperature"] # Update j, except in the "distributed SEI resistance" model, where j will be # found by solving an algebraic equation. # (In the "distributed SEI resistance" model, we have already defined j) - if ( - self.reaction == "lithium-ion main" - and self.options["particle size"] == "distribution" - ): - # For "particle-size distribution" models, additional steps (R-averaging) - # are necessary to calculate j - j, j_distribution = self._get_PSD_current_densities(j0, ne, eta_r, T) + j = self._get_kinetics(j0, ne, eta_r, T) + + if j.domain == [self.domain.lower() + " particle size"]: + # If j depends on particle size, get size-dependent "distribution" + # variables first variables.update( - self._get_standard_PSD_interfacial_current_variables(j_distribution) + self._get_standard_size_distribution_interfacial_current_variables(j) + ) + variables.update( + self._get_standard_size_distribution_exchange_current_variables(j0) + ) + variables.update( + self._get_standard_size_distribution_overpotential_variables(eta_r) + ) + variables.update( + self._get_standard_size_distribution_ocp_variables(ocp, dUdT) ) - - else: - j = self._get_kinetics(j0, ne, eta_r, T) variables.update(self._get_standard_interfacial_current_variables(j)) From 009752f23ed16a8901fdfd59561abb0d9ba8f2f6 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 6 Jul 2021 17:31:30 +0100 Subject: [PATCH 53/67] reduce code duplication in quick_plot --- .../inverse_kinetics/inverse_butler_volmer.py | 5 -- pybamm/plotting/quick_plot.py | 46 ++++++---------- .../test_lithium_ion/test_mpm.py | 46 ---------------- .../test_leading_size_distribution.py | 52 ------------------- .../test_inverse_butler_volmer.py | 8 ++- 5 files changed, 21 insertions(+), 136 deletions(-) delete mode 100644 tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py 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 32065f06ca..40f5ef7282 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -29,11 +29,6 @@ class InverseButlerVolmer(BaseInterface): def __init__(self, param, domain, reaction, options=None): super().__init__(param, domain, reaction) - if options is None: - options = { - "SEI film resistance": "none", - "particle size": "single" - } self.options = options def get_coupled_variables(self, variables): diff --git a/pybamm/plotting/quick_plot.py b/pybamm/plotting/quick_plot.py index 269473a03a..c656c9f4bd 100644 --- a/pybamm/plotting/quick_plot.py +++ b/pybamm/plotting/quick_plot.py @@ -257,10 +257,8 @@ def set_output_variables(self, output_variables, solutions): self.spatial_variable_dict = {} self.first_dimensional_spatial_variable = {} self.second_dimensional_spatial_variable = {} - self.is_x_r = {} + self.x_first_and_y_second = {} self.is_y_z = {} - self.is_x_R = {} - self.is_R_r = {} # Calculate subplot positions based on number of variables supplied self.subplot_positions = {} @@ -341,37 +339,23 @@ def set_output_variables(self, output_variables, solutions): self.second_dimensional_spatial_variable[variable_tuple] = ( second_spatial_var_value * self.spatial_factor ) - if first_spatial_var_name == "r" and second_spatial_var_name == "x": - self.is_x_r[variable_tuple] = True - self.is_y_z[variable_tuple] = False - self.is_x_R[variable_tuple] = False - self.is_R_r[variable_tuple] = False - elif ( - first_spatial_var_name == "y" and second_spatial_var_name == "z" + # different order based on whether the domains + # are x-r, x-z or y-z, etc + if ( + first_spatial_var_name in ("r", "R") and + second_spatial_var_name == "x" ): - self.is_x_r[variable_tuple] = False - self.is_y_z[variable_tuple] = True - self.is_x_R[variable_tuple] = False - self.is_R_r[variable_tuple] = False - elif ( - first_spatial_var_name == "R" and second_spatial_var_name == "x" - ): - self.is_x_r[variable_tuple] = False + self.x_first_and_y_second[variable_tuple] = False self.is_y_z[variable_tuple] = False - self.is_x_R[variable_tuple] = True - self.is_R_r[variable_tuple] = False elif ( - first_spatial_var_name == "r" and second_spatial_var_name == "R" + first_spatial_var_name == "y" and + second_spatial_var_name == "z" ): - self.is_x_r[variable_tuple] = False - self.is_y_z[variable_tuple] = False - self.is_x_R[variable_tuple] = False - self.is_R_r[variable_tuple] = True + self.x_first_and_y_second[variable_tuple] = True + self.is_y_z[variable_tuple] = True else: - self.is_x_r[variable_tuple] = False + self.x_first_and_y_second[variable_tuple] = True self.is_y_z[variable_tuple] = False - self.is_x_R[variable_tuple] = False - self.is_R_r[variable_tuple] = False # Store variables and subplot position self.variables[variable_tuple] = variables @@ -416,7 +400,7 @@ def reset_axis(self): x_max = self.first_dimensional_spatial_variable[key][-1] elif variable_lists[0][0].dimensions == 2: # different order based on whether the domains are x-r, x-z or y-z, etc - if (self.is_x_r[key] or self.is_x_R[key] or self.is_R_r[key]) is True: + if self.x_first_and_y_second[key] is False: x_min = self.second_dimensional_spatial_variable[key][0] x_max = self.second_dimensional_spatial_variable[key][-1] y_min = self.first_dimensional_spatial_variable[key][0] @@ -573,7 +557,7 @@ def plot(self, t, dynamic=False): # there can only be one entry in the variable list variable = variable_lists[0][0] # different order based on whether the domains are x-r, x-z or y-z, etc - if (self.is_x_r[key] or self.is_x_R[key] or self.is_R_r[key]) is True: + if self.x_first_and_y_second[key] is False: x_name = list(spatial_vars.keys())[1][0] y_name = list(spatial_vars.keys())[0][0] x = self.second_dimensional_spatial_variable[key] @@ -736,7 +720,7 @@ def slider_update(self, t): # there can only be one entry in the variable list variable = self.variables[key][0][0] vmin, vmax = self.variable_limits[key] - if (self.is_x_r[key] or self.is_x_R[key] or self.is_R_r[key]) is True: + if self.x_first_and_y_second[key] is False: x = self.second_dimensional_spatial_variable[key] y = self.first_dimensional_spatial_variable[key] var = variable(time_in_seconds, **spatial_vars, warn=False) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 0c11681351..0f4bd42fb4 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -135,52 +135,6 @@ def test_conservation_each_electrode(self): np.testing.assert_array_almost_equal(neg_Li[0], neg_Li[1], decimal=14) np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=14) - def set_distribution_params_for_test(self, param): - import numpy as np - - R_n_dim = param["Negative particle radius [m]"] - R_p_dim = param["Positive particle radius [m]"] - sd_a_n = 0.3 - sd_a_p = 0.3 - - # Min and max radii - R_min_n = 0 - R_min_p = 0 - R_max_n = 1 + sd_a_n * 5 - R_max_p = 1 + sd_a_p * 5 - - def lognormal(R, R_av, sd): - mu_ln = pybamm.log(R_av ** 2 / pybamm.sqrt(R_av ** 2 + sd ** 2)) - sigma_ln = pybamm.sqrt(pybamm.log(1 + sd ** 2 / R_av ** 2)) - return ( - pybamm.exp(-((pybamm.log(R) - mu_ln) ** 2) / (2 * sigma_ln ** 2)) - / pybamm.sqrt(2 * np.pi * sigma_ln ** 2) - / (R) - ) - - # Set the dimensional (area-weighted) particle-size distributions - def f_a_dist_n_dim(R): - return lognormal(R, R_n_dim, sd_a_n * R_n_dim) - - def f_a_dist_p_dim(R): - return lognormal(R, R_p_dim, sd_a_p * R_p_dim) - - # Append to parameter set - param.update( - { - "Negative minimum particle radius [m]": R_min_n * R_n_dim, - "Positive minimum particle radius [m]": R_min_p * R_p_dim, - "Negative maximum particle radius [m]": R_max_n * R_n_dim, - "Positive maximum particle radius [m]": R_max_p * R_p_dim, - "Negative area-weighted " - + "particle-size distribution [m-1]": f_a_dist_n_dim, - "Positive area-weighted " - + "particle-size distribution [m-1]": f_a_dist_p_dim, - }, - check_already_exists=False, - ) - return param - if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py b/tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py deleted file mode 100644 index c91dd7615f..0000000000 --- a/tests/unit/test_models/test_submodels/test_electrode/test_ohm/test_leading_size_distribution.py +++ /dev/null @@ -1,52 +0,0 @@ -# -# Test leading-order ohm submodel -# - -import pybamm -import tests -import unittest - - -class TestLeadingOrderSizeDistribution(unittest.TestCase): - def test_public_functions(self): - param = pybamm.LithiumIonParameters() - - a = pybamm.Scalar(0) - variables = { - "Current collector current density": a, - "Negative current collector potential": a, - "X-averaged negative" - + " electrode total interfacial current density": a, - "Sum of x-averaged negative" - + " electrode interfacial current densities": a, - } - submodel = pybamm.electrode.ohm.LeadingOrderSizeDistribution(param, "Negative") - std_tests = tests.StandardSubModelTests(submodel, variables) - std_tests.test_all() - - variables = { - "Current collector current density": a, - "Negative current collector potential": a, - "Negative electrode current density": pybamm.PrimaryBroadcast( - a, ["negative electrode"] - ), - "X-averaged positive electrode surface potential difference": a, - "X-averaged positive electrolyte potential": a, - "X-averaged positive" - + " electrode total interfacial current density": a, - "Sum of x-averaged positive" - + " electrode interfacial current densities": a - } - submodel = pybamm.electrode.ohm.LeadingOrderSizeDistribution(param, "Positive") - 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() diff --git a/tests/unit/test_models/test_submodels/test_interface/test_inverse_kinetics/test_inverse_butler_volmer.py b/tests/unit/test_models/test_submodels/test_interface/test_inverse_kinetics/test_inverse_butler_volmer.py index fcb300cf42..526ded0ca9 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_inverse_kinetics/test_inverse_butler_volmer.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_inverse_kinetics/test_inverse_butler_volmer.py @@ -19,8 +19,12 @@ def test_public_functions(self): "Negative electrolyte concentration": a, "Current collector current density": a, } + options = { + "SEI film resistance": "none", + "particle size": "single" + } submodel = pybamm.interface.inverse_kinetics.InverseButlerVolmer( - param, "Negative", "lithium-ion main" + param, "Negative", "lithium-ion main", options ) std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() @@ -33,7 +37,7 @@ def test_public_functions(self): "Current collector current density": a, } submodel = pybamm.interface.inverse_kinetics.InverseButlerVolmer( - param, "Positive", "lithium-ion main" + param, "Positive", "lithium-ion main", options ) std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() From 2ca98d49a37d13643c2b7a93740fa564197bda65 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 6 Jul 2021 17:51:18 +0100 Subject: [PATCH 54/67] remove some mpm tests --- .../test_lithium_ion/test_mpm.py | 19 ----- .../test_lithium_ion/test_mpm.py | 81 ------------------- 2 files changed, 100 deletions(-) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 0f4bd42fb4..cf598117de 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -18,25 +18,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - def test_basic_processing_1plus1D(self): - options = {"current collector": "potential pair", "dimensionality": 1} - - model = pybamm.lithium_ion.MPM(options) - 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.R_n: 5, - var.R_p: 5, - var.y: 5, - var.z: 5, - } - modeltest = tests.StandardModelTest(model, var_pts=var_pts) - modeltest.test_all(skip_output_tests=True) - def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.MPM(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 7d983650bc..ada5533d38 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -16,15 +16,6 @@ def test_well_posed(self): model.build_model() model.check_well_posedness() - def test_well_posed_2plus1D(self): - options = {"current collector": "potential pair", "dimensionality": 1} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - - options = {"current collector": "potential pair", "dimensionality": 2} - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - def test_lumped_thermal_model_1D(self): options = {"thermal": "lumped"} model = pybamm.lithium_ion.MPM(options) @@ -35,24 +26,6 @@ def test_x_full_thermal_not_implemented(self): with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_x_full_Nplus1D_not_implemented(self): - # 1plus1D - options = { - "current collector": "potential pair", - "dimensionality": 1, - "thermal": "x-full", - } - with self.assertRaises(NotImplementedError): - pybamm.lithium_ion.MPM(options) - # 2plus1D - options = { - "current collector": "potential pair", - "dimensionality": 2, - "thermal": "x-full", - } - with self.assertRaises(NotImplementedError): - pybamm.lithium_ion.MPM(options) - def test_lumped_thermal_1plus1D(self): options = { "current collector": "potential pair", @@ -62,15 +35,6 @@ def test_lumped_thermal_1plus1D(self): model = pybamm.lithium_ion.MPM(options) model.check_well_posedness() - def test_lumped_thermal_2plus1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 2, - "thermal": "lumped", - } - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - def test_thermal_1plus1D(self): options = { "current collector": "potential pair", @@ -80,25 +44,11 @@ def test_thermal_1plus1D(self): model = pybamm.lithium_ion.MPM(options) model.check_well_posedness() - def test_thermal_2plus1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 2, - "thermal": "x-lumped", - } - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - def test_particle_uniform(self): options = {"particle": "uniform profile"} model = pybamm.lithium_ion.MPM(options) model.check_well_posedness() - def test_particle_shape_user(self): - options = {"particle shape": "user"} - with self.assertRaises(NotImplementedError): - pybamm.lithium_ion.MPM(options) - def test_loss_active_material_stress_negative(self): options = {"loss of active material": ("stress-driven", "none")} with self.assertRaises(NotImplementedError): @@ -114,37 +64,6 @@ def test_loss_active_material_stress_both(self): with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_electrolyte_options(self): - options = {"electrolyte conductivity": "full"} - with self.assertRaisesRegex(pybamm.OptionError, "electrolyte conductivity"): - pybamm.lithium_ion.MPM(options) - - def test_new_model(self): - model = pybamm.lithium_ion.MPM({"thermal": "x-lumped"}) - new_model = model.new_copy() - model_T_eqn = model.rhs[model.variables["Volume-averaged cell temperature"]] - new_model_T_eqn = new_model.rhs[ - new_model.variables["Volume-averaged cell temperature"] - ] - self.assertEqual(new_model_T_eqn.id, model_T_eqn.id) - self.assertEqual(new_model.name, model.name) - self.assertEqual(new_model.use_jacobian, model.use_jacobian) - self.assertEqual(new_model.convert_to_format, model.convert_to_format) - self.assertEqual(new_model.timescale.id, model.timescale.id) - - # with custom submodels - model = pybamm.lithium_ion.MPM({"thermal": "x-lumped"}, build=False) - model.submodels[ - "negative particle" - ] = pybamm.particle.FastSingleSizeDistribution( - model.param, "Negative" - ) - model.build_model() - new_model = model.new_copy() - new_model_cs_eqn = list(new_model.rhs.values())[1] - model_cs_eqn = list(model.rhs.values())[1] - self.assertEqual(new_model_cs_eqn.id, model_cs_eqn.id) - def test_well_posed_reversible_plating_with_porosity(self): options = { "lithium plating": "reversible", From b9eb06e2c65813109badf3810df43f8e6c9ae8b1 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 7 Jul 2021 09:57:42 +0100 Subject: [PATCH 55/67] change MPM init and reduce tests --- .../full_battery_models/lithium_ion/mpm.py | 27 +--------------- .../full_battery_models/lithium_ion/spm.py | 3 +- .../test_lithium_ion/test_mpm.py | 32 ------------------- .../test_lithium_ion/test_mpm.py | 29 ++++++----------- 4 files changed, 13 insertions(+), 78 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index 69e97f9634..aa3c481e34 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -43,32 +43,7 @@ def __init__( else: options["particle size"] = "distribution" options["surface form"] = "algebraic" - super(SPM, self).__init__(options, name) - - # For degradation models we use the "x-average" form since this is a - # reduced-order model with uniform current density in the electrodes - self.x_average = True - - self.set_external_circuit_submodel() - self.set_porosity_submodel() - self.set_crack_submodel() - self.set_active_material_submodel() - self.set_tortuosity_submodels() - self.set_convection_submodel() - self.set_interfacial_submodel() - self.set_other_reaction_submodels_to_zero() - self.set_particle_submodel() - self.set_negative_electrode_submodel() - self.set_electrolyte_submodel() - self.set_positive_electrode_submodel() - self.set_thermal_submodel() - self.set_current_collector_submodel() - - self.set_sei_submodel() - self.set_lithium_plating_submodel() - - if build: - self.build_model() + super().__init__(options, name, build) pybamm.citations.register("Kirk2020") pybamm.citations.register("Kirk2021") diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index 91200627c1..706c6c3471 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -56,7 +56,8 @@ def __init__(self, options=None, name="Single Particle Model", build=True): if build: self.build_model() - pybamm.citations.register("Marquis2019") + if self.__class__ != "MPM": + pybamm.citations.register("Marquis2019") def set_convection_submodel(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index cf598117de..b39a9cf315 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -18,24 +18,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - def test_basic_processing_2plus1D(self): - options = {"current collector": "potential pair", "dimensionality": 2} - model = pybamm.lithium_ion.MPM(options) - 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.R_n: 5, - var.R_p: 5, - var.y: 5, - var.z: 5, - } - modeltest = tests.StandardModelTest(model, var_pts=var_pts) - modeltest.test_all(skip_output_tests=True) - def test_optimisations(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) @@ -57,20 +39,6 @@ def test_set_up(self): optimtest.set_up_model(to_python=True) optimtest.set_up_model(to_python=False) - def test_zero_current(self): - options = {"thermal": "isothermal"} - model = pybamm.lithium_ion.MPM(options) - parameter_values = model.default_parameter_values - parameter_values.update({"Current function [A]": 0}) - modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) - modeltest.test_all() - - def test_thermal_lumped(self): - options = {"thermal": "lumped"} - model = pybamm.lithium_ion.MPM(options) - modeltest = tests.StandardModelTest(model) - modeltest.test_all() - def test_particle_uniform(self): options = {"particle": "uniform profile"} model = pybamm.lithium_ion.MPM(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index ada5533d38..deb328c8f8 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -26,15 +26,6 @@ def test_x_full_thermal_not_implemented(self): with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_lumped_thermal_1plus1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 1, - "thermal": "lumped", - } - model = pybamm.lithium_ion.MPM(options) - model.check_well_posedness() - def test_thermal_1plus1D(self): options = { "current collector": "potential pair", @@ -49,22 +40,22 @@ def test_particle_uniform(self): model = pybamm.lithium_ion.MPM(options) model.check_well_posedness() - def test_loss_active_material_stress_negative(self): + def test_loss_active_material_stress_negative_not_implemented(self): options = {"loss of active material": ("stress-driven", "none")} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_loss_active_material_stress_positive(self): + def test_loss_active_material_stress_positive_not_implemented(self): options = {"loss of active material": ("none", "stress-driven")} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_loss_active_material_stress_both(self): + def test_loss_active_material_stress_both_not_implemented(self): options = {"loss of active material": "stress-driven"} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_well_posed_reversible_plating_with_porosity(self): + def test_well_posed_reversible_plating_with_porosity_not_implemented(self): options = { "lithium plating": "reversible", "lithium plating porosity change": "true", @@ -123,34 +114,34 @@ def test_ec_reaction_limited_not_implemented(self): class TestMPMWithCrack(unittest.TestCase): - def test_well_posed_negative_cracking(self): + def test_well_posed_negative_cracking_not_implemented(self): options = {"particle mechanics": ("swelling and cracking", "none")} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_well_posed_positive_cracking(self): + def test_well_posed_positive_cracking_not_implemented(self): options = {"particle mechanics": ("none", "swelling and cracking")} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_well_posed_both_cracking(self): + def test_well_posed_both_cracking_not_implemented(self): options = {"particle mechanics": "swelling and cracking"} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_well_posed_both_swelling_only(self): + def test_well_posed_both_swelling_only_not_implemented(self): options = {"particle mechanics": "swelling only"} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) class TestMPMWithPlating(unittest.TestCase): - def test_well_posed_reversible_plating(self): + def test_well_posed_reversible_plating_not_implemented(self): options = {"lithium plating": "reversible"} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) - def test_well_posed_irreversible_plating(self): + def test_well_posed_irreversible_plating_not_implemented(self): options = {"lithium plating": "irreversible"} with self.assertRaises(NotImplementedError): pybamm.lithium_ion.MPM(options) From 339b9f08c432b1cc1b7aa93b5628f38afe2266ab Mon Sep 17 00:00:00 2001 From: tobykirk Date: Wed, 7 Jul 2021 11:23:49 +0100 Subject: [PATCH 56/67] moved particle size params to geometric parameters --- pybamm/expression_tree/unary_operators.py | 10 +- pybamm/geometry/battery_geometry.py | 9 +- .../active_material/base_active_material.py | 4 +- .../submodels/interface/base_interface.py | 10 +- pybamm/parameters/geometric_parameters.py | 103 ++++++++++++++- pybamm/parameters/lithium_ion_parameters.py | 117 +++++------------- .../test_unary_operators.py | 15 +-- 7 files changed, 150 insertions(+), 118 deletions(-) diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 632ab249fd..949f0d62d7 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1399,7 +1399,7 @@ def r_average(symbol): return Integral(symbol, r) / Integral(v, r) -def size_average(symbol, param): +def size_average(symbol): """convenience function for averaging over particle size R using the area-weighted particle-size distribution. @@ -1407,9 +1407,6 @@ def size_average(symbol, param): ---------- symbol : :class:`pybamm.Symbol` The function to be averaged - param : :class:`pybamm.LithiumIonParameters` - The parameter object containing the area-weighted particle-size distributions - f_a_dist_n and f_a_dist_p. Returns ------- :class:`Symbol` @@ -1453,10 +1450,11 @@ def size_average(symbol, param): auxiliary_domains=symbol.auxiliary_domains, coord_sys="cartesian", ) + geo = pybamm.geometric_parameters if ["negative particle size"] in list(symbol.domains.values()): - f_a_dist = param.f_a_dist_n(R) + f_a_dist = geo.f_a_dist_n(R) elif ["positive particle size"] in list(symbol.domains.values()): - f_a_dist = param.f_a_dist_p(R) + f_a_dist = geo.f_a_dist_p(R) # take average using Integral and distribution f_a_dist return Integral(f_a_dist * symbol, R) / Integral(f_a_dist, R) diff --git a/pybamm/geometry/battery_geometry.py b/pybamm/geometry/battery_geometry.py index 521e3d30f9..7de5adb7af 100644 --- a/pybamm/geometry/battery_geometry.py +++ b/pybamm/geometry/battery_geometry.py @@ -51,11 +51,10 @@ def battery_geometry( options is not None and options["particle size"] == "distribution" ): - param = pybamm.LithiumIonParameters(options) - R_min_n = param.R_min_n - R_min_p = param.R_min_p - R_max_n = param.R_max_n - R_max_p = param.R_max_p + R_min_n = geo.R_min_n + R_min_p = geo.R_min_p + R_max_n = geo.R_max_n + R_max_p = geo.R_max_p geometry.update( { "negative particle size": { diff --git a/pybamm/models/submodels/active_material/base_active_material.py b/pybamm/models/submodels/active_material/base_active_material.py index 8a7714b477..c5fa526eab 100644 --- a/pybamm/models/submodels/active_material/base_active_material.py +++ b/pybamm/models/submodels/active_material/base_active_material.py @@ -80,7 +80,7 @@ def _get_standard_active_material_variables(self, eps_solid): R_dim = self.param.R_n_dimensional(x * self.param.L_x) elif self.options["particle size"] == "distribution": R_n = pybamm.standard_spatial_vars.R_n - R = pybamm.size_average(R_n, self.param) + R = pybamm.size_average(R_n) R_dim = R * self.param.R_n_typ a_typ = self.param.a_n_typ elif self.domain == "Positive": @@ -90,7 +90,7 @@ def _get_standard_active_material_variables(self, eps_solid): R_dim = self.param.R_p_dimensional(x * self.param.L_x) elif self.options["particle size"] == "distribution": R_p = pybamm.standard_spatial_vars.R_p - R = pybamm.size_average(R_p, self.param) + R = pybamm.size_average(R_p) R_dim = R * self.param.R_p_typ a_typ = self.param.a_p_typ diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index fd9aab98c8..09338ac945 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -291,7 +291,7 @@ def _get_standard_interfacial_current_variables(self, j): # Size average. For j variables that depend on particle size, see # "_get_standard_size_distribution_interfacial_current_variables" - j = pybamm.size_average(j, self.param) + j = pybamm.size_average(j) # Average, and broadcast if necessary if j.domain == []: @@ -450,7 +450,7 @@ def _get_standard_exchange_current_variables(self, j0): # Size average. For j0 variables that depend on particle size, see # "_get_standard_size_distribution_exchange_current_variables" - j0 = pybamm.size_average(j0, self.param) + j0 = pybamm.size_average(j0) # X-average, and broadcast if necessary if j0.domain == []: @@ -536,7 +536,7 @@ def _get_standard_overpotential_variables(self, eta_r): # Size average. For eta_r variables that depend on particle size, see # "_get_standard_size_distribution_overpotential_variables" - eta_r = pybamm.size_average(eta_r, self.param) + eta_r = pybamm.size_average(eta_r) # X-average, and broadcast if necessary eta_r_av = pybamm.x_average(eta_r) @@ -649,7 +649,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): """ # Size average. For ocp variables that depend on particle size, see # "_get_standard_size_distribution_ocp_variables" - ocp = pybamm.size_average(ocp, self.param) + ocp = pybamm.size_average(ocp) # X-average, and broadcast if necessary if ocp.domain == []: @@ -664,7 +664,7 @@ def _get_standard_ocp_variables(self, ocp, dUdT): ocp_av = pybamm.x_average(ocp) # Size average - dUdT = pybamm.size_average(dUdT, self.param) + dUdT = pybamm.size_average(dUdT) dUdT_av = pybamm.x_average(dUdT) diff --git a/pybamm/parameters/geometric_parameters.py b/pybamm/parameters/geometric_parameters.py index 43b329083c..47a90291ef 100644 --- a/pybamm/parameters/geometric_parameters.py +++ b/pybamm/parameters/geometric_parameters.py @@ -11,13 +11,17 @@ class GeometricParameters(BaseParameters): Layout: 1. Dimensional Parameters - 2. Dimensionless Parameters + 2. Dimensional Functions + 3. Scalings + 4. Dimensionless Parameters + 5. Dimensionless Functions """ def __init__(self): - # Set parameters + # Set parameters and scales self._set_dimensional_parameters() + self._set_scales() self._set_dimensionless_parameters() def _set_dimensional_parameters(self): @@ -51,8 +55,7 @@ def _set_dimensional_parameters(self): self.A_tab_p = self.L_tab_p * self.L_cp # Area of negative tab # Microscale geometry - # Note: parameters related to the particles in li-ion cells are defined - # in lithium_ion_parameters.py. The definition of the surface area to + # Note: for li-ion cells, the definition of the surface area to # volume ratio is overwritten in lithium_ion_parameters.py to be computed # based on the assumed particle shape self.a_n_dim = pybamm.Parameter( @@ -75,6 +78,60 @@ def _set_dimensional_parameters(self): "Positive electrode Bruggeman coefficient (electrode)" ) + # Particle-size distribution geometry + self.R_min_n_dim = pybamm.Parameter("Negative minimum particle radius [m]") + self.R_min_p_dim = pybamm.Parameter("Positive minimum particle radius [m]") + self.R_max_n_dim = pybamm.Parameter("Negative maximum particle radius [m]") + self.R_max_p_dim = pybamm.Parameter("Positive maximum particle radius [m]") + self.sd_a_n_dim = pybamm.Parameter( + "Negative area-weighted particle-size standard deviation [m]" + ) + self.sd_a_p_dim = pybamm.Parameter( + "Positive area-weighted particle-size standard deviation [m]" + ) + + def R_n_dimensional(self, x): + """Negative particle radius as a function of through-cell distance""" + inputs = {"Through-cell distance (x_n) [m]": x} + return pybamm.FunctionParameter("Negative particle radius [m]", inputs) + + def R_p_dimensional(self, x): + """Positive particle radius as a function of through-cell distance""" + inputs = {"Through-cell distance (x_p) [m]": x} + return pybamm.FunctionParameter("Positive particle radius [m]", inputs) + + def f_a_dist_n_dimensional(self, R): + """ + Dimensional negative electrode area-weighted particle-size distribution + """ + inputs = { + "Negative particle-size variable [m]": R, + } + return pybamm.FunctionParameter( + "Negative area-weighted particle-size distribution [m-1]", inputs, + ) + + def f_a_dist_p_dimensional(self, R): + """ + Dimensional positive electrode area-weighted particle-size distribution + """ + inputs = { + "Positive particle-size variable [m]": R, + } + return pybamm.FunctionParameter( + "Positive area-weighted particle-size distribution [m-1]", inputs, + ) + + def _set_scales(self): + """Define the scales used in the non-dimensionalisation scheme""" + + # Microscale geometry + # Note: these scales are necessary here to non-dimensionalise the + # particle size distributions. + # Use typical values at electrode/current collector interface. + self.R_n_typ = self.R_n_dimensional(0) + self.R_p_typ = self.R_p_dimensional(self.L_x) + def _set_dimensionless_parameters(self): """Defines the dimensionless parameters.""" @@ -103,5 +160,43 @@ def _set_dimensionless_parameters(self): self.centre_y_tab_p = self.Centre_y_tab_p / self.L_z self.centre_z_tab_p = self.Centre_z_tab_p / self.L_z + # Particle-size distribution geometry + self.R_min_n = self.R_min_n_dim / self.R_n_typ + self.R_min_p = self.R_min_p_dim / self.R_p_typ + self.R_max_n = self.R_max_n_dim / self.R_n_typ + self.R_max_p = self.R_max_p_dim / self.R_p_typ + self.sd_a_n = self.sd_a_n_dim / self.R_n_typ + self.sd_a_p = self.sd_a_p_dim / self.R_p_typ + + def R_n(self, x): + """ + Dimensionless negative particle radius as a function of dimensionless + position x + """ + x_dim = x * self.L_x + return self.R_n_dimensional(x_dim) / self.R_n_typ + + def R_p(self, x): + """ + Dimensionless positive particle radius as a function of dimensionless + position x + """ + x_dim = x * self.L_x + return self.R_p_dimensional(x_dim) / self.R_p_typ + + def f_a_dist_n(self, R): + """ + Dimensionless negative electrode area-weighted particle-size distribution + """ + R_dim = R * self.R_n_typ + return self.f_a_dist_n_dimensional(R_dim) * self.R_n_typ + + def f_a_dist_p(self, R): + """ + Dimensionless positive electrode area-weighted particle-size distribution + """ + R_dim = R * self.R_p_typ + return self.f_a_dist_p_dimensional(R_dim) * self.R_p_typ + geometric_parameters = GeometricParameters() diff --git a/pybamm/parameters/lithium_ion_parameters.py b/pybamm/parameters/lithium_ion_parameters.py index d858cacbb5..b2c5d37b72 100644 --- a/pybamm/parameters/lithium_ion_parameters.py +++ b/pybamm/parameters/lithium_ion_parameters.py @@ -111,9 +111,12 @@ def _set_dimensional_parameters(self): ) # Microscale geometry - # Note: the particle radius in the electrodes can be set as a function - # of through-cell position, so is defined later as a function, along with - # the surface area to volume ratio + # Note: the surface area to volume ratio is defined later with the function + # parameters. The particle size as a function of through-cell position is + # already defined in geometric_parameters.py + self.R_n_dimensional = self.geo.R_n_dimensional + self.R_p_dimensional = self.geo.R_p_dimensional + inputs = { "Through-cell distance (x_n) [m]": pybamm.standard_spatial_vars.x_n * self.L_x @@ -149,6 +152,16 @@ def _set_dimensional_parameters(self): self.b_s_n = self.geo.b_s_n self.b_s_p = self.geo.b_s_p + # Particle-size distribution parameters + self.R_min_n_dim = self.geo.R_min_n_dim + self.R_min_p_dim = self.geo.R_min_p_dim + self.R_max_n_dim = self.geo.R_max_n_dim + self.R_max_p_dim = self.geo.R_max_p_dim + self.sd_a_n_dim = self.geo.sd_a_n_dim + self.sd_a_p_dim = self.geo.sd_a_p_dim + self.f_a_dist_n_dimensional = self.geo.f_a_dist_n_dimensional + self.f_a_dist_p_dimensional = self.geo.f_a_dist_p_dimensional + # Electrochemical reactions self.ne_n = pybamm.Parameter("Negative electrode electrons in reaction") self.ne_p = pybamm.Parameter("Positive electrode electrons in reaction") @@ -331,18 +344,6 @@ def _set_dimensional_parameters(self): "Positive electrode reaction-driven LAM factor [m3.mol-1]" ) - # Particle-size distribution parameters - self.R_min_n_dim = pybamm.Parameter("Negative minimum particle radius [m]") - self.R_min_p_dim = pybamm.Parameter("Positive minimum particle radius [m]") - self.R_max_n_dim = pybamm.Parameter("Negative maximum particle radius [m]") - self.R_max_p_dim = pybamm.Parameter("Positive maximum particle radius [m]") - self.sd_a_n_dim = pybamm.Parameter( - "Negative area-weighted particle-size standard deviation [m]" - ) - self.sd_a_p_dim = pybamm.Parameter( - "Positive area-weighted particle-size standard deviation [m]" - ) - def D_e_dimensional(self, c_e, T): """Dimensional diffusivity in electrolyte""" inputs = {"Electrolyte concentration [mol.m-3]": c_e, "Temperature [K]": T} @@ -469,16 +470,6 @@ def dUdT_p_dimensional(self, sto): "Positive electrode OCP entropic change [V.K-1]", inputs ) - def R_n_dimensional(self, x): - """Negative particle radius as a function of through-cell distance""" - inputs = {"Through-cell distance (x_n) [m]": x} - return pybamm.FunctionParameter("Negative particle radius [m]", inputs) - - def R_p_dimensional(self, x): - """Positive particle radius as a function of through-cell distance""" - inputs = {"Through-cell distance (x_p) [m]": x} - return pybamm.FunctionParameter("Positive particle radius [m]", inputs) - def epsilon_s_n(self, x): """Negative electrode active material volume fraction""" inputs = {"Through-cell distance (x_n) [m]": x * self.L_x} @@ -507,34 +498,12 @@ def c_p_init_dimensional(self, x): "Initial concentration in positive electrode [mol.m-3]", inputs ) - def f_a_dist_n_dimensional(self, R): - """ - Dimensional negative electrode area-weighted particle-size distribution - """ - inputs = { - "Negative particle-size variable [m]": R, - } - return pybamm.FunctionParameter( - "Negative area-weighted particle-size distribution [m-1]", inputs, - ) - - def f_a_dist_p_dimensional(self, R): - """ - Dimensional positive electrode area-weighted particle-size distribution - """ - inputs = { - "Positive particle-size variable [m]": R, - } - return pybamm.FunctionParameter( - "Positive area-weighted particle-size distribution [m-1]", inputs, - ) - def _set_scales(self): """Define the scales used in the non-dimensionalisation scheme""" - # Microscale (typical values at electrode/current collector interface) - self.R_n_typ = self.R_n_dimensional(0) - self.R_p_typ = self.R_p_dimensional(self.L_x) + # Microscale + self.R_n_typ = self.geo.R_n_typ + self.R_p_typ = self.geo.R_p_typ if self.options["particle shape"] == "spherical": self.a_n_typ = 3 * self.epsilon_s_n(0) / self.R_n_typ self.a_p_typ = 3 * self.epsilon_s_p(1) / self.R_p_typ @@ -654,16 +623,20 @@ def _set_dimensionless_parameters(self): self.centre_z_tab_p = self.geo.centre_z_tab_p # Microscale geometry + self.R_n = self.geo.R_n + self.R_p = self.geo.R_p self.a_R_n = self.a_n_typ * self.R_n_typ self.a_R_p = self.a_p_typ * self.R_p_typ - # Particle-size distribution parameters - self.R_min_n = self.R_min_n_dim / self.R_n_typ - self.R_min_p = self.R_min_p_dim / self.R_p_typ - self.R_max_n = self.R_max_n_dim / self.R_n_typ - self.R_max_p = self.R_max_p_dim / self.R_p_typ - self.sd_a_n = self.sd_a_n_dim / self.R_n_typ - self.sd_a_p = self.sd_a_p_dim / self.R_p_typ + # Particle-size distribution geometry + self.R_min_n = self.geo.R_min_n + self.R_min_p = self.geo.R_min_p + self.R_max_n = self.geo.R_max_n + self.R_max_p = self.geo.R_max_p + self.sd_a_n = self.geo.sd_a_n + self.sd_a_p = self.geo.sd_a_p + self.f_a_dist_n = self.geo.f_a_dist_n + self.f_a_dist_p = self.geo.f_a_dist_p # Electrode Properties self.sigma_cn = ( @@ -1006,36 +979,6 @@ def dUdT_p(self, c_s_p): sto = c_s_p return self.dUdT_p_dimensional(sto) * self.Delta_T / self.potential_scale - def R_n(self, x): - """ - Dimensionless negative particle radius as a function of dimensionless - position x - """ - x_dim = x * self.L_x - return self.R_n_dimensional(x_dim) / self.R_n_typ - - def R_p(self, x): - """ - Dimensionless positive particle radius as a function of dimensionless - position x - """ - x_dim = x * self.L_x - return self.R_p_dimensional(x_dim) / self.R_p_typ - - def f_a_dist_n(self, R): - """ - Dimensionless negative electrode area-weighted particle-size distribution - """ - R_dim = R * self.R_n_typ - return self.f_a_dist_n_dimensional(R_dim) * self.R_n_typ - - def f_a_dist_p(self, R): - """ - Dimensionless positive electrode area-weighted particle-size distribution - """ - R_dim = R * self.R_p_typ - return self.f_a_dist_p_dimensional(R_dim) * self.R_p_typ - def c_n_init(self, x): """ Dimensionless initial concentration as a function of dimensionless position x. diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 034d3e04c5..ae2f1e02ca 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -628,11 +628,10 @@ def test_x_average(self): self.assertEqual(av_a.children[1].id, l_p.id) def test_size_average(self): - param = pybamm.LithiumIonParameters() # no domain a = pybamm.Scalar(1) - average_a = pybamm.size_average(a, param) + average_a = pybamm.size_average(a) self.assertEqual(average_a.id, a.id) b = pybamm.FullBroadcast( @@ -644,27 +643,25 @@ def test_size_average(self): } ) # no "particle size" domain - average_b = pybamm.size_average(b, param) + average_b = pybamm.size_average(b) self.assertEqual(average_b.id, b.id) # primary or secondary broadcast to "particle size" domain average_a = pybamm.size_average( - pybamm.PrimaryBroadcast(a, "negative particle size"), - param + pybamm.PrimaryBroadcast(a, "negative particle size") ) self.assertEqual(average_a.evaluate(), np.array([1])) a = pybamm.Symbol("a", domain="negative particle") average_a = pybamm.size_average( - pybamm.SecondaryBroadcast(a, "negative particle size"), - param + pybamm.SecondaryBroadcast(a, "negative particle size") ) self.assertEqual(average_a.id, a.id) for domain in [["negative particle size"], ["positive particle size"]]: a = pybamm.Symbol("a", domain=domain) R = pybamm.SpatialVariable("R", domain) - av_a = pybamm.size_average(a, param) + av_a = pybamm.size_average(a) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertIsInstance(av_a.children[1], pybamm.Integral) @@ -678,7 +675,7 @@ def test_size_average(self): ValueError, """Can't take the size-average of a symbol that evaluates on edges""" ): - pybamm.size_average(symbol_on_edges, param) + pybamm.size_average(symbol_on_edges) def test_r_average(self): a = pybamm.Scalar(1) From 82ea2888f9b1377afb2e29eb01643f41e294db02 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 16 Jul 2021 15:22:15 +0100 Subject: [PATCH 57/67] move default standard deviations --- pybamm/parameters/size_distribution_parameters.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pybamm/parameters/size_distribution_parameters.py b/pybamm/parameters/size_distribution_parameters.py index 3c57770ff9..295a04f223 100644 --- a/pybamm/parameters/size_distribution_parameters.py +++ b/pybamm/parameters/size_distribution_parameters.py @@ -11,8 +11,8 @@ def get_size_distribution_parameters( param, R_n_av=None, R_p_av=None, - sd_n=None, - sd_p=None, + sd_n=0.3, + sd_p=0.3, R_min_n=None, R_min_p=None, R_max_n=None, @@ -64,10 +64,6 @@ def get_size_distribution_parameters( R_n_av = R_n_av or R_n_typ R_p_av = R_p_av or R_p_typ - # Standard deviations (scaled by the mean radius) - sd_n = sd_n or 0.3 - sd_p = sd_p or 0.3 - # Minimum radii R_min_n = R_min_n or np.max([0, 1 - sd_n * 5]) R_min_p = R_min_p or np.max([0, 1 - sd_p * 5]) From edf67bb2e8f9f5e5d6649e424b6cc3011805955f Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 16 Jul 2021 15:34:42 +0100 Subject: [PATCH 58/67] change definition of particle flux --- .../size_distribution/fickian_single_distribution.py | 11 ++++------- .../integration/test_models/standard_output_tests.py | 11 +++++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py index a0f45fccaf..1109d47a29 100644 --- a/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/fickian_single_distribution.py @@ -106,18 +106,15 @@ def get_coupled_variables(self, variables): [self.domain.lower() + " particle size"], ) T_k_xav = pybamm.PrimaryBroadcast(T_k_xav, [self.domain.lower() + " particle"],) - R = pybamm.PrimaryBroadcast( - R_spatial_variable, [self.domain.lower() + " particle"], - ) if self.domain == "Negative": N_s_xav_distribution = -self.param.D_n( c_s_xav_distribution, T_k_xav - ) * pybamm.grad(c_s_xav_distribution) / R + ) * pybamm.grad(c_s_xav_distribution) elif self.domain == "Positive": N_s_xav_distribution = -self.param.D_p( c_s_xav_distribution, T_k_xav - ) * pybamm.grad(c_s_xav_distribution) / R + ) * pybamm.grad(c_s_xav_distribution) # Standard R-averaged flux variables. Average using the area-weighted # distribution @@ -166,13 +163,13 @@ def set_rhs(self, variables): self.rhs = { c_s_xav_distribution: -(1 / self.param.C_n) * pybamm.div(N_s_xav_distribution) - / R + / R ** 2 } elif self.domain == "Positive": self.rhs = { c_s_xav_distribution: -(1 / self.param.C_p) * pybamm.div(N_s_xav_distribution) - / R + / R ** 2 } def set_boundary_conditions(self, variables): diff --git a/tests/integration/test_models/standard_output_tests.py b/tests/integration/test_models/standard_output_tests.py index fde4f57f26..d5a83e85e3 100644 --- a/tests/integration/test_models/standard_output_tests.py +++ b/tests/integration/test_models/standard_output_tests.py @@ -304,6 +304,8 @@ def test_concentration_increase_decrease(self): t, x_n, x_p, r_n, r_p = self.t, self.x_n, self.x_p, self.r_n, self.r_p + tol = 1e-16 + if self.model.options["particle"] in ["quadratic profile", "quartic profile"]: # For the assumed polynomial concentration profiles the values # can increase/decrease within the particle as the polynomial shifts, @@ -331,6 +333,7 @@ def test_concentration_increase_decrease(self): self.c_s_p_dist(t[-1], r=r_p, R=R_p) - self.c_s_p_dist(t[0], r=r_p, R=R_p) ) + tol = 1e-15 else: neg_diff = self.c_s_n(t[1:], x_n, r_n) - self.c_s_n(t[:-1], x_n, r_n) pos_diff = self.c_s_p(t[1:], x_p, r_p) - self.c_s_p(t[:-1], x_p, r_p) @@ -338,13 +341,13 @@ def test_concentration_increase_decrease(self): pos_end_vs_start = self.c_s_p(t[-1], x_p, r_p) - self.c_s_p(t[0], x_p, r_p) if self.operating_condition == "discharge": - np.testing.assert_array_less(neg_diff, 1e-16) - np.testing.assert_array_less(-1e-16, pos_diff) + np.testing.assert_array_less(neg_diff, tol) + np.testing.assert_array_less(-tol, pos_diff) np.testing.assert_array_less(neg_end_vs_start, 0) np.testing.assert_array_less(0, pos_end_vs_start) elif self.operating_condition == "charge": - np.testing.assert_array_less(-1e-16, neg_diff) - np.testing.assert_array_less(pos_diff, 1e-16) + np.testing.assert_array_less(-tol, neg_diff) + np.testing.assert_array_less(pos_diff, tol) np.testing.assert_array_less(0, neg_end_vs_start) np.testing.assert_array_less(pos_end_vs_start, 0) elif self.operating_condition == "off": From de5afe3134410e43f75b14934144299c35e3f35f Mon Sep 17 00:00:00 2001 From: tobykirk Date: Fri, 16 Jul 2021 15:35:03 +0100 Subject: [PATCH 59/67] fix notebooks --- examples/notebooks/using-submodels.ipynb | 27 ++++++++++-------------- examples/scripts/custom_model.py | 4 ++-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/examples/notebooks/using-submodels.ipynb b/examples/notebooks/using-submodels.ipynb index 152f80b193..5dd216d669 100644 --- a/examples/notebooks/using-submodels.ipynb +++ b/examples/notebooks/using-submodels.ipynb @@ -403,7 +403,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the Single Particle Model, the overpotential can be obtianed by inverting the Butler-Volmer relation, so we choose the `InverseButlerVolmer` submodel for the interface, with the \"main\" lithium-ion reaction. Because of how the current is implemented, we also need to separately specify the `CurrentForInverseButlerVolmer` submodel" + "In the Single Particle Model, the overpotential can be obtianed by inverting the Butler-Volmer relation, so we choose the `InverseButlerVolmer` submodel for the interface, with the \"main\" lithium-ion reaction (and default lithium ion options). Because of how the current is implemented, we also need to separately specify the `CurrentForInverseButlerVolmer` submodel" ] }, { @@ -414,10 +414,14 @@ "source": [ "model.submodels[\n", " \"negative interface\"\n", - "] = pybamm.interface.InverseButlerVolmer(model.param, \"Negative\", \"lithium-ion main\")\n", + "] = pybamm.interface.InverseButlerVolmer(\n", + " model.param, \"Negative\", \"lithium-ion main\", options=model.options\n", + ")\n", "model.submodels[\n", " \"positive interface\"\n", - "] = pybamm.interface.InverseButlerVolmer(model.param, \"Positive\", \"lithium-ion main\")\n", + "] = pybamm.interface.InverseButlerVolmer(\n", + " model.param, \"Positive\", \"lithium-ion main\", options=model.options\n", + ")\n", "model.submodels[\n", " \"negative interface current\"\n", "] = pybamm.interface.CurrentForInverseButlerVolmer(\n", @@ -540,23 +544,14 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" + "display_name": "Python 2.7.17 64-bit", + "name": "python375jvsc74a57bd0fd69f43f58546b570e94fd7eba7b65e6bcc7a5bbc4eab0408017d18902915d69" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" + "version": "" } }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/examples/scripts/custom_model.py b/examples/scripts/custom_model.py index 777023be9b..98ddb8389d 100644 --- a/examples/scripts/custom_model.py +++ b/examples/scripts/custom_model.py @@ -36,10 +36,10 @@ model.param, "Positive", "uniform profile" ) model.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( - model.param, "Negative", "lithium-ion main" + model.param, "Negative", "lithium-ion main", options=model.options ) model.submodels["positive interface"] = pybamm.interface.InverseButlerVolmer( - model.param, "Positive", "lithium-ion main" + model.param, "Positive", "lithium-ion main", options=model.options ) model.submodels[ "negative interface current" From 3fbe86ae81e2e69cd6ed09e0b45ac22bf0d31c88 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 20 Jul 2021 14:57:42 +0100 Subject: [PATCH 60/67] add broadcast tests with particle size --- .../test_expression_tree/test_broadcasts.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_expression_tree/test_broadcasts.py b/tests/unit/test_expression_tree/test_broadcasts.py index 271c6c5a17..18ec3c6588 100644 --- a/tests/unit/test_expression_tree/test_broadcasts.py +++ b/tests/unit/test_expression_tree/test_broadcasts.py @@ -27,6 +27,12 @@ def test_primary_broadcast(self): broad_a.auxiliary_domains, {"secondary": ["negative electrode"], "tertiary": ["current collector"]}, ) + broad_a = pybamm.PrimaryBroadcast(a, ["negative particle size"]) + self.assertEqual(broad_a.domain, ["negative particle size"]) + self.assertEqual( + broad_a.auxiliary_domains, + {"secondary": ["negative electrode"], "tertiary": ["current collector"]}, + ) a = pybamm.Symbol("a", domain="current collector") with self.assertRaisesRegex( @@ -38,6 +44,11 @@ def test_primary_broadcast(self): pybamm.DomainError, "Primary broadcast from electrode" ): pybamm.PrimaryBroadcast(a, "current collector") + a = pybamm.Symbol("a", domain="negative particle size") + with self.assertRaisesRegex( + pybamm.DomainError, "Primary broadcast from particle size" + ): + pybamm.PrimaryBroadcast(a, "negative electrode") a = pybamm.Symbol("a", domain="negative particle") with self.assertRaisesRegex( pybamm.DomainError, "Cannot do primary broadcast from particle domain" @@ -57,6 +68,15 @@ def test_secondary_broadcast(self): {"secondary": ["negative electrode"], "tertiary": ["current collector"]}, ) self.assertTrue(broad_a.broadcasts_to_nodes) + broad_a = pybamm.SecondaryBroadcast(a, ["negative particle size"]) + self.assertEqual(broad_a.domain, ["negative particle"]) + self.assertEqual( + broad_a.auxiliary_domains, + { + "secondary": ["negative particle size"], + "tertiary": ["current collector"] + }, + ) with self.assertRaises(NotImplementedError): broad_a.reduce_one_dimension() @@ -69,6 +89,11 @@ def test_secondary_broadcast(self): pybamm.DomainError, "Secondary broadcast from particle" ): pybamm.SecondaryBroadcast(a, "current collector") + a = pybamm.Symbol("a", domain="negative particle size") + with self.assertRaisesRegex( + pybamm.DomainError, "Secondary broadcast from particle size" + ): + pybamm.SecondaryBroadcast(a, "negative particle") a = pybamm.Symbol("a", domain="negative electrode") with self.assertRaisesRegex( pybamm.DomainError, "Secondary broadcast from electrode" From c7472ece1e981233a1f3f6105fff8441954f56f2 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 20 Jul 2021 14:59:48 +0100 Subject: [PATCH 61/67] add particle size processed variable tests --- tests/__init__.py | 2 + tests/shared.py | 39 ++++++- .../test_solvers/test_processed_variable.py | 105 ++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 0810a14b7a..e66107f67a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,10 +11,12 @@ from .shared import ( get_mesh_for_testing, get_p2d_mesh_for_testing, + get_size_distribution_mesh_for_testing, get_1p1d_mesh_for_testing, get_2p1d_mesh_for_testing, get_discretisation_for_testing, get_p2d_discretisation_for_testing, + get_size_distribution_disc_for_testing, get_1p1d_discretisation_for_testing, get_2p1d_discretisation_for_testing, get_unit_2p1D_mesh_for_testing, diff --git a/tests/shared.py b/tests/shared.py index e366526a8b..1bdf9631da 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -42,7 +42,7 @@ def mass_matrix(self, symbol, boundary_conditions): def get_mesh_for_testing( - xpts=None, rpts=10, ypts=15, zpts=15, geometry=None, cc_submesh=None + xpts=None, rpts=10, Rpts=10, ypts=15, zpts=15, geometry=None, cc_submesh=None ): param = pybamm.ParameterValues( values={ @@ -57,6 +57,12 @@ def get_mesh_for_testing( "Negative electrode thickness [m]": 0.3, "Separator thickness [m]": 0.3, "Positive electrode thickness [m]": 0.3, + "Negative particle radius [m]": 0.5, + "Positive particle radius [m]": 0.5, + "Negative minimum particle radius [m]": 0.0, + "Negative maximum particle radius [m]": 1.0, + "Positive minimum particle radius [m]": 0.0, + "Positive maximum particle radius [m]": 1.0, } ) @@ -70,6 +76,8 @@ def get_mesh_for_testing( "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "negative particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "positive particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "negative particle size": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "positive particle size": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "current collector": pybamm.MeshGenerator(pybamm.SubMesh0D), } if cc_submesh: @@ -88,6 +96,8 @@ def get_mesh_for_testing( var.r_p: rpts, var.y: ypts, var.z: zpts, + var.R_n: Rpts, + var.R_p: Rpts, } return pybamm.Mesh(geometry, submesh_types, var_pts) @@ -98,6 +108,25 @@ def get_p2d_mesh_for_testing(xpts=None, rpts=10): return get_mesh_for_testing(xpts=xpts, rpts=rpts, geometry=geometry) +def get_size_distribution_mesh_for_testing( + xpts=None, + rpts=10, + Rpts=10, + zpts=15, + cc_submesh=pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), +): + options = {"particle size": "distribution"} + geometry = pybamm.battery_geometry(options=options, current_collector_dimension=1) + return get_mesh_for_testing( + xpts=xpts, + rpts=rpts, + Rpts=Rpts, + zpts=zpts, + geometry=geometry, + cc_submesh=cc_submesh + ) + + def get_1p1d_mesh_for_testing( xpts=None, rpts=10, @@ -175,6 +204,8 @@ def get_discretisation_for_testing( "macroscale": SpatialMethodForTesting(), "negative particle": SpatialMethodForTesting(), "positive particle": SpatialMethodForTesting(), + "negative particle size": SpatialMethodForTesting(), + "positive particle size": SpatialMethodForTesting(), "current collector": cc_method(), } return pybamm.Discretisation(mesh, spatial_methods) @@ -184,6 +215,12 @@ def get_p2d_discretisation_for_testing(xpts=None, rpts=10): return get_discretisation_for_testing(mesh=get_p2d_mesh_for_testing(xpts, rpts)) +def get_size_distribution_disc_for_testing(xpts=None, rpts=10, Rpts=10, zpts=15): + return get_discretisation_for_testing( + mesh=get_size_distribution_mesh_for_testing(xpts, rpts, Rpts, zpts) + ) + + def get_1p1d_discretisation_for_testing(xpts=None, rpts=10, zpts=15): return get_discretisation_for_testing( mesh=get_1p1d_mesh_for_testing(xpts, rpts, zpts) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 6d692da6ea..9024ebbedb 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -192,6 +192,111 @@ def test_processed_variable_2D_x_r(self): np.reshape(y_sol, [len(r_sol), len(x_sol), len(t_sol)]), ) + def test_processed_variable_2D_x_R(self): + var = pybamm.Variable( + "var", + domain=["negative particle size"], + auxiliary_domains={"secondary": ["negative electrode"]}, + ) + R = pybamm.SpatialVariable( + "R", + domain=["negative particle size"], + auxiliary_domains={"secondary": ["negative electrode"]}, + ) + x = pybamm.SpatialVariable("x", domain=["negative electrode"]) + + disc = tests.get_size_distribution_disc_for_testing() + disc.set_variable_slices([var]) + x_sol = disc.process_symbol(x).entries[:, 0] + R_sol = disc.process_symbol(R).entries[:, 0] + # Keep only the first iteration of entries + R_sol = R_sol[: len(R_sol) // len(x_sol)] + var_sol = disc.process_symbol(var) + t_sol = np.linspace(0, 1) + y_sol = np.ones(len(x_sol) * len(R_sol))[:, np.newaxis] * np.linspace(0, 5) + + var_casadi = to_casadi(var_sol, y_sol) + processed_var = pybamm.ProcessedVariable( + [var_sol], + [var_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), + warn=False, + ) + np.testing.assert_array_equal( + processed_var.entries, + np.reshape(y_sol, [len(R_sol), len(x_sol), len(t_sol)]), + ) + + def test_processed_variable_2D_R_z(self): + var = pybamm.Variable( + "var", + domain=["negative particle size"], + auxiliary_domains={"secondary": ["current collector"]}, + ) + R = pybamm.SpatialVariable( + "R", + domain=["negative particle size"], + auxiliary_domains={"secondary": ["current collector"]}, + ) + z = pybamm.SpatialVariable("z", domain=["current collector"]) + + disc = tests.get_size_distribution_disc_for_testing() + disc.set_variable_slices([var]) + z_sol = disc.process_symbol(z).entries[:, 0] + R_sol = disc.process_symbol(R).entries[:, 0] + # Keep only the first iteration of entries + R_sol = R_sol[: len(R_sol) // len(z_sol)] + var_sol = disc.process_symbol(var) + t_sol = np.linspace(0, 1) + y_sol = np.ones(len(z_sol) * len(R_sol))[:, np.newaxis] * np.linspace(0, 5) + + var_casadi = to_casadi(var_sol, y_sol) + processed_var = pybamm.ProcessedVariable( + [var_sol], + [var_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), + warn=False, + ) + np.testing.assert_array_equal( + processed_var.entries, + np.reshape(y_sol, [len(R_sol), len(z_sol), len(t_sol)]), + ) + + def test_processed_variable_2D_r_R(self): + var = pybamm.Variable( + "var", + domain=["negative particle"], + auxiliary_domains={"secondary": ["negative particle size"]}, + ) + r = pybamm.SpatialVariable( + "r", + domain=["negative particle"], + auxiliary_domains={"secondary": ["negative particle size"]}, + ) + R = pybamm.SpatialVariable("R", domain=["negative particle size"]) + + disc = tests.get_size_distribution_disc_for_testing() + disc.set_variable_slices([var]) + r_sol = disc.process_symbol(r).entries[:, 0] + R_sol = disc.process_symbol(R).entries[:, 0] + # Keep only the first iteration of entries + r_sol = r_sol[: len(r_sol) // len(R_sol)] + var_sol = disc.process_symbol(var) + t_sol = np.linspace(0, 1) + y_sol = np.ones(len(r_sol) * len(R_sol))[:, np.newaxis] * np.linspace(0, 5) + + var_casadi = to_casadi(var_sol, y_sol) + processed_var = pybamm.ProcessedVariable( + [var_sol], + [var_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), + warn=False, + ) + np.testing.assert_array_equal( + processed_var.entries, + np.reshape(y_sol, [len(r_sol), len(R_sol), len(t_sol)]), + ) + def test_processed_variable_2D_x_z(self): var = pybamm.Variable( "var", From 3e9261d326a450e800858d970dbf6c308173c3bd Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 20 Jul 2021 15:01:37 +0100 Subject: [PATCH 62/67] add mpm tests for coverage --- .../full_battery_models/lithium_ion/mpm.py | 25 +++++++++++++------ .../test_lithium_ion/test_mpm.py | 23 +++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/pybamm/models/full_battery_models/lithium_ion/mpm.py index aa3c481e34..600fbce9a6 100644 --- a/pybamm/models/full_battery_models/lithium_ion/mpm.py +++ b/pybamm/models/full_battery_models/lithium_ion/mpm.py @@ -40,6 +40,24 @@ def __init__( "particle size": "distribution", "surface form": "algebraic" } + elif ( + "particle size" in options and + options["particle size"] != "distribution" + ): + raise pybamm.OptionError( + "particle size must be 'distribution' for MPM not '{}'".format( + options["particle size"] + ) + ) + elif ( + "surface form" in options and + options["surface form"] != "algebraic" + ): + raise pybamm.OptionError( + "surface form must be 'algebraic' for MPM not '{}'".format( + options["surface form"] + ) + ) else: options["particle size"] = "distribution" options["surface form"] = "algebraic" @@ -50,13 +68,6 @@ def __init__( def set_particle_submodel(self): - if self.options["particle size"] != "distribution": - raise pybamm.OptionError( - "particle size must be 'distribution' for MPM not '{}'".format( - self.options["particle size"] - ) - ) - if self.options["particle"] == "Fickian diffusion": submod_n = pybamm.particle.FickianSingleSizeDistribution( self.param, "Negative" diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index deb328c8f8..aa0bb57556 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -16,6 +16,16 @@ def test_well_posed(self): model.build_model() model.check_well_posedness() + def test_default_parameter_values(self): + # check default parameters are added correctly + model = pybamm.lithium_ion.MPM() + self.assertEqual( + model.default_parameter_values[ + "Negative area-weighted mean particle radius [m]" + ], + 1E-05 + ) + def test_lumped_thermal_model_1D(self): options = {"thermal": "lumped"} model = pybamm.lithium_ion.MPM(options) @@ -40,6 +50,19 @@ def test_particle_uniform(self): model = pybamm.lithium_ion.MPM(options) model.check_well_posedness() + def test_necessary_options(self): + options = {"particle size": "single"} + with self.assertRaises(pybamm.OptionError): + pybamm.lithium_ion.MPM(options) + options = {"surface form": "none"} + with self.assertRaises(pybamm.OptionError): + pybamm.lithium_ion.MPM(options) + + def test_nonspherical_particle_not_implemented(self): + options = {"particle shape": "user"} + with self.assertRaises(NotImplementedError): + pybamm.lithium_ion.MPM(options) + def test_loss_active_material_stress_negative_not_implemented(self): options = {"loss of active material": ("stress-driven", "none")} with self.assertRaises(NotImplementedError): From 85989dae5b3568c245927d63ddc8b0157cfbe1fe Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 22 Jul 2021 16:12:06 +0100 Subject: [PATCH 63/67] remove untested code for size distr in DFN --- .../size_distribution/base_distribution.py | 67 +++++-------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/pybamm/models/submodels/particle/size_distribution/base_distribution.py b/pybamm/models/submodels/particle/size_distribution/base_distribution.py index 379480d503..34fbe1135e 100644 --- a/pybamm/models/submodels/particle/size_distribution/base_distribution.py +++ b/pybamm/models/submodels/particle/size_distribution/base_distribution.py @@ -66,26 +66,21 @@ def _get_distribution_variables(self, R): sd_a = pybamm.x_average(sd_a) sd_v = pybamm.x_average(sd_v) - # X-averaged distributions, or broadcast - if R.auxiliary_domains["secondary"] == [self.domain.lower() + " electrode"]: - f_a_dist_xav = pybamm.x_average(f_a_dist) - f_v_dist_xav = pybamm.x_average(f_v_dist) - f_num_dist_xav = pybamm.x_average(f_num_dist) - else: - f_a_dist_xav = f_a_dist - f_v_dist_xav = f_v_dist - f_num_dist_xav = f_num_dist - - # broadcast - f_a_dist = pybamm.SecondaryBroadcast( - f_a_dist_xav, [self.domain.lower() + " electrode"] - ) - f_v_dist = pybamm.SecondaryBroadcast( - f_v_dist_xav, [self.domain.lower() + " electrode"] - ) - f_num_dist = pybamm.SecondaryBroadcast( - f_num_dist_xav, [self.domain.lower() + " electrode"] - ) + # X-averaged distributions + f_a_dist_xav = f_a_dist + f_v_dist_xav = f_v_dist + f_num_dist_xav = f_num_dist + + # broadcast + f_a_dist = pybamm.SecondaryBroadcast( + f_a_dist_xav, [self.domain.lower() + " electrode"] + ) + f_v_dist = pybamm.SecondaryBroadcast( + f_v_dist_xav, [self.domain.lower() + " electrode"] + ) + f_num_dist = pybamm.SecondaryBroadcast( + f_num_dist_xav, [self.domain.lower() + " electrode"] + ) variables = { self.domain + " particle sizes": R, @@ -199,38 +194,6 @@ def _get_standard_concentration_distribution_variables(self, c_s): "tertiary": self.domain.lower() + " electrode", }, ) - elif c_s.domain == [ - self.domain.lower() + " particle size" - ] and c_s.auxiliary_domains["secondary"] == [ - self.domain.lower() + " electrode" - ]: - # Surface concentration distribution variables - c_s_surf_distribution = c_s - c_s_surf_xav_distribution = pybamm.x_average(c_s) - - # X-avg concentration distribution - c_s_xav_distribution = pybamm.PrimaryBroadcast( - c_s_surf_xav_distribution, [self.domain.lower() + " particle"] - ) - - # Concentration distribution in all domains. - # NOTE: currently variables can only have 3 domains, so current collector - # is excluded, i.e. pushed off domain list - c_s_distribution = pybamm.PrimaryBroadcast( - c_s_surf_distribution, [self.domain.lower() + " particle"] - ) - else: - c_s_distribution = c_s - - # x-average the *tertiary* domain. Do manually using Integral - x = pybamm.SpatialVariable("x", domain=[self.domain.lower() + " electrode"]) - v = pybamm.ones_like(c_s) - l = pybamm.Integral(v, x) - c_s_xav_distribution = pybamm.Integral(c_s, x) / l - - # Surface concentration distribution variables - c_s_surf_distribution = pybamm.surf(c_s) - c_s_surf_xav_distribution = pybamm.x_average(c_s_surf_distribution) variables = { self.domain From 7b871529e3bdaefd1fde2fa67f6b5eaea593294f Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 22 Jul 2021 16:16:23 +0100 Subject: [PATCH 64/67] tests to increase coverage --- tests/shared.py | 2 +- .../test_size_distribution_parameters.py | 56 +++++++++++++++++++ .../test_solvers/test_processed_variable.py | 20 ++++++- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_parameters/test_size_distribution_parameters.py diff --git a/tests/shared.py b/tests/shared.py index 1bdf9631da..b4efb8f9df 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -67,7 +67,7 @@ def get_mesh_for_testing( ) if geometry is None: - geometry = pybamm.battery_geometry() + geometry = pybamm.battery_geometry(options={"particle size": "distribution"}) param.process_geometry(geometry) submesh_types = { diff --git a/tests/unit/test_parameters/test_size_distribution_parameters.py b/tests/unit/test_parameters/test_size_distribution_parameters.py new file mode 100644 index 0000000000..e8e625ae66 --- /dev/null +++ b/tests/unit/test_parameters/test_size_distribution_parameters.py @@ -0,0 +1,56 @@ +# +# Tests particle size distribution parameters are loaded into a parameter set +# and give expected values +# +import pybamm + +import unittest +import numpy as np + + +class TestSizeDistributionParameters(unittest.TestCase): + def test_parameter_values(self): + values = pybamm.lithium_ion.BaseModel().default_parameter_values + param = pybamm.LithiumIonParameters() + + # add distribution parameter values + values = pybamm.get_size_distribution_parameters(values) + + # check dimensionless parameters + + # min and max radii + np.testing.assert_almost_equal( + values.evaluate(param.R_min_n), 0.0, 3 + ) + np.testing.assert_almost_equal( + values.evaluate(param.R_min_p), 0.0, 3 + ) + np.testing.assert_almost_equal( + values.evaluate(param.R_max_n), 2.5, 3 + ) + np.testing.assert_almost_equal( + values.evaluate(param.R_max_p), 2.5, 3 + ) + + # standard deviations + np.testing.assert_almost_equal( + values.evaluate(param.sd_a_n), 0.3, 3 + ) + np.testing.assert_almost_equal( + values.evaluate(param.sd_a_p), 0.3, 3 + ) + + # check function parameters (size distributions) evaluate + R_test = pybamm.Scalar(1.0) + values.evaluate(param.f_a_dist_n(R_test)) + values.evaluate(param.f_a_dist_p(R_test)) + + +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_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 9024ebbedb..781f836826 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -553,7 +553,7 @@ def test_processed_var_1D_interpolation(self): ) np.testing.assert_array_almost_equal(processed_x(x=x_sol), x_sol[:, np.newaxis]) - # On microscale + # In particles r_n = pybamm.Matrix( disc.mesh["negative particle"].nodes, domain="negative particle" ) @@ -570,6 +570,24 @@ def test_processed_var_1D_interpolation(self): processed_r_n(0, r=np.linspace(0, 1))[:, 0], np.linspace(0, 1) ) + # On size domain + R_n = pybamm.Matrix( + disc.mesh["negative particle size"].nodes, + domain="negative particle size" + ) + R_n.mesh = disc.mesh["negative particle size"] + R_n_casadi = to_casadi(R_n, y_sol) + processed_R_n = pybamm.ProcessedVariable( + [R_n], + [R_n_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), + warn=False, + ) + np.testing.assert_array_equal(R_n.entries[:, 0], processed_R_n.entries[:, 0]) + np.testing.assert_array_almost_equal( + processed_R_n(0, R=np.linspace(0, 1))[:, 0], np.linspace(0, 1) + ) + def test_processed_var_1D_fixed_t_interpolation(self): var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) From 8a964b3ef47a9e63edc5e252ef1ba16bc6b08cb8 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Tue, 27 Jul 2021 16:57:18 +0100 Subject: [PATCH 65/67] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 467433dd6c..ec120432fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM) ## Features - +- Added a new lithium-ion model `MPM` or Many-Particle Model, with a distribution of particle sizes in each electrode. ([#1529](https://github.com/pybamm-team/PyBaMM/pull/1529)) +- Added 2 new submodels for lithium transport in a size distribution of electrode particles: Fickian diffusion (`FickianSingleSizeDistribution`) and uniform concentration profile (`FastSingleSizeDistribution`). ([#1529](https://github.com/pybamm-team/PyBaMM/pull/1529)) +- Added a "particle size" domain to the default lithium-ion geometry, including plotting capabilities (`QuickPlot`) and processing of variables (`ProcessedVariable`). ([#1529](https://github.com/pybamm-team/PyBaMM/pull/1529)) - Added fitted expressions for OCPs for the Chen2020 parameter set ([#1526](https://github.com/pybamm-team/PyBaMM/pull/1497)) - Added `initial_soc` argument to `Simualtion.solve` for specifying the initial SOC when solving a model ([#1512](https://github.com/pybamm-team/PyBaMM/pull/1512)) - Added `print_name` to some symbols ([#1495](https://github.com/pybamm-team/PyBaMM/pull/1495), [#1497](https://github.com/pybamm-team/PyBaMM/pull/1497)) From 7c50b998e3517fdb7ff3130cdb4746809b7444c8 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 29 Jul 2021 14:04:54 +0100 Subject: [PATCH 66/67] reduced code duplication in processed var tests --- .../test_solvers/test_processed_variable.py | 138 +++++------------- 1 file changed, 37 insertions(+), 101 deletions(-) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 781f836826..636ad3ef34 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -26,6 +26,37 @@ def to_casadi(var_pybamm, y, inputs=None): return var_casadi +def process_and_check_2D_variable( + var, first_spatial_var, second_spatial_var, disc=None +): + # first_spatial_var should be on the "smaller" domain, i.e "r" for an "r-x" variable + if disc is None: + disc = tests.get_discretisation_for_testing() + disc.set_variable_slices([var]) + + first_sol = disc.process_symbol(first_spatial_var).entries[:, 0] + second_sol = disc.process_symbol(second_spatial_var).entries[:, 0] + + # Keep only the first iteration of entries + first_sol = first_sol[: len(first_sol) // len(second_sol)] + var_sol = disc.process_symbol(var) + t_sol = np.linspace(0, 1) + y_sol = np.ones(len(second_sol) * len(first_sol))[:, np.newaxis] * np.linspace(0, 5) + + var_casadi = to_casadi(var_sol, y_sol) + processed_var = pybamm.ProcessedVariable( + [var_sol], + [var_casadi], + pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), + warn=False, + ) + np.testing.assert_array_equal( + processed_var.entries, + np.reshape(y_sol, [len(first_sol), len(second_sol), len(t_sol)]), + ) + return y_sol, first_sol, second_sol, t_sol + + class TestProcessedVariable(unittest.TestCase): def test_processed_variable_0D(self): # without space @@ -171,28 +202,9 @@ def test_processed_variable_2D_x_r(self): ) disc = tests.get_p2d_discretisation_for_testing() - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - r_sol = disc.process_symbol(r).entries[:, 0] - # Keep only the first iteration of entries - r_sol = r_sol[: len(r_sol) // len(x_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + process_and_check_2D_variable(var, r, x, disc=disc) - var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(r_sol), len(x_sol), len(t_sol)]), - ) - - def test_processed_variable_2D_x_R(self): + def test_processed_variable_2D_R_x(self): var = pybamm.Variable( "var", domain=["negative particle size"], @@ -206,26 +218,7 @@ def test_processed_variable_2D_x_R(self): x = pybamm.SpatialVariable("x", domain=["negative electrode"]) disc = tests.get_size_distribution_disc_for_testing() - disc.set_variable_slices([var]) - x_sol = disc.process_symbol(x).entries[:, 0] - R_sol = disc.process_symbol(R).entries[:, 0] - # Keep only the first iteration of entries - R_sol = R_sol[: len(R_sol) // len(x_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(x_sol) * len(R_sol))[:, np.newaxis] * np.linspace(0, 5) - - var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(R_sol), len(x_sol), len(t_sol)]), - ) + process_and_check_2D_variable(var, R, x, disc=disc) def test_processed_variable_2D_R_z(self): var = pybamm.Variable( @@ -241,26 +234,7 @@ def test_processed_variable_2D_R_z(self): z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_size_distribution_disc_for_testing() - disc.set_variable_slices([var]) - z_sol = disc.process_symbol(z).entries[:, 0] - R_sol = disc.process_symbol(R).entries[:, 0] - # Keep only the first iteration of entries - R_sol = R_sol[: len(R_sol) // len(z_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(z_sol) * len(R_sol))[:, np.newaxis] * np.linspace(0, 5) - - var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(R_sol), len(z_sol), len(t_sol)]), - ) + process_and_check_2D_variable(var, R, z, disc=disc) def test_processed_variable_2D_r_R(self): var = pybamm.Variable( @@ -276,26 +250,7 @@ def test_processed_variable_2D_r_R(self): R = pybamm.SpatialVariable("R", domain=["negative particle size"]) disc = tests.get_size_distribution_disc_for_testing() - disc.set_variable_slices([var]) - r_sol = disc.process_symbol(r).entries[:, 0] - R_sol = disc.process_symbol(R).entries[:, 0] - # Keep only the first iteration of entries - r_sol = r_sol[: len(r_sol) // len(R_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(r_sol) * len(R_sol))[:, np.newaxis] * np.linspace(0, 5) - - var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(r_sol), len(R_sol), len(t_sol)]), - ) + process_and_check_2D_variable(var, r, R, disc=disc) def test_processed_variable_2D_x_z(self): var = pybamm.Variable( @@ -311,26 +266,7 @@ def test_processed_variable_2D_x_z(self): z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_1p1d_discretisation_for_testing() - disc.set_variable_slices([var]) - z_sol = disc.process_symbol(z).entries[:, 0] - x_sol = disc.process_symbol(x).entries[:, 0] - # Keep only the first iteration of entries - x_sol = x_sol[: len(x_sol) // len(z_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(x_sol) * len(z_sol))[:, np.newaxis] * np.linspace(0, 5) - - var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(x_sol), len(z_sol), len(t_sol)]), - ) + y_sol, x_sol, z_sol, t_sol = process_and_check_2D_variable(var, x, z, disc=disc) # On edges x_s_edge = pybamm.Matrix( From 515590da2a7b4d77201d2f2d38ad6f805b483c01 Mon Sep 17 00:00:00 2001 From: tobykirk Date: Thu, 29 Jul 2021 14:36:39 +0100 Subject: [PATCH 67/67] remove unusued variable to fix codacy --- tests/unit/test_solvers/test_processed_variable.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 636ad3ef34..35f111e7be 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -267,6 +267,7 @@ def test_processed_variable_2D_x_z(self): disc = tests.get_1p1d_discretisation_for_testing() y_sol, x_sol, z_sol, t_sol = process_and_check_2D_variable(var, x, z, disc=disc) + del x_sol # On edges x_s_edge = pybamm.Matrix(