def test_combine_geometries(self): geometry1Dmacro = pybamm.Geometry1DMacro() geometry1Dmicro = pybamm.Geometry1DMicro() geometry = pybamm.Geometry(geometry1Dmacro, geometry1Dmicro) self.assertEqual( set(geometry.keys()), set( [ "negative electrode", "separator", "positive electrode", "negative particle", "positive particle", "current collector", ] ), ) # update with custom geometry whole_cell = ["negative electrode", "separator", "positive electrode"] x = pybamm.SpatialVariable("x", whole_cell) custom_geometry = { "negative electrode": { "primary": {x: {"min": pybamm.Scalar(1), "max": pybamm.Scalar(2)}} } } geometry = pybamm.Geometry( geometry1Dmacro, geometry1Dmicro, custom_geometry=custom_geometry ) self.assertEqual( geometry["negative electrode"], custom_geometry["negative electrode"] )
def test_combine_geometries_strings(self): geometry = pybamm.Geometry("1D macro", "1D micro") self.assertEqual( set(geometry.keys()), set( [ "negative electrode", "separator", "positive electrode", "negative particle", "positive particle", "current collector", ] ), ) geometry = pybamm.Geometry("3D macro", "1D micro") self.assertEqual( set(geometry.keys()), set( [ "negative electrode", "separator", "positive electrode", "negative particle", "positive particle", "current collector", ] ), )
def default_geometry(self): if self.options["dimensionality"] == 0: return pybamm.Geometry("1D macro", "1+1D micro") elif self.options["dimensionality"] == 1: return pybamm.Geometry("1+1D macro", "1+1D micro") elif self.options["dimensionality"] == 2: return pybamm.Geometry("2+1D macro", "1+1D micro")
def default_geometry(self): dimensionality = self.options["dimensionality"] if dimensionality == 0: return pybamm.Geometry("1D macro", "1D micro") elif dimensionality == 1: return pybamm.Geometry("1+1D macro", "(1+0)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+0)+1D micro")
def test_processed_variable_1D_unknown_domain(self): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") geometry = pybamm.Geometry({ "SEI layer": { x: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(1) } } }) submesh_types = {"SEI layer": pybamm.Uniform1DSubMesh} var_pts = {x: 100} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) nt = 100 y_sol = np.zeros((var_pts[x], nt)) solution = pybamm.Solution( np.linspace(0, 1, nt), y_sol, pybamm.BaseModel(), {}, np.linspace(0, 1, 1), np.zeros((var_pts[x])), "test", ) c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] c_casadi = to_casadi(c, y_sol) pybamm.ProcessedVariable([c], [c_casadi], solution, warn=False)
def test_extrapolate_on_nonuniform_grid(self): geometry = pybamm.Geometry("1D micro") submesh_types = { "negative particle": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh), "positive particle": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh), } var = pybamm.standard_spatial_vars rpts = 10 var_pts = {var.r_n: rpts, var.r_p: rpts} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) method_options = {"extrapolation": {"order": "linear", "use bcs": False}} spatial_methods = {"negative particle": pybamm.FiniteVolume(method_options)} disc = pybamm.Discretisation(mesh, spatial_methods) var = pybamm.Variable("var", domain="negative particle") surf_eqn = pybamm.surf(var) disc.set_variable_slices([var]) surf_eqn_disc = disc.process_symbol(surf_eqn) micro_submesh = mesh["negative particle"] # check constant extrapolates to constant constant_y = np.ones_like(micro_submesh[0].nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( surf_eqn_disc.evaluate(None, constant_y), 1 ) # check linear variable extrapolates correctly linear_y = micro_submesh[0].nodes y_surf = micro_submesh[0].edges[-1] np.testing.assert_array_almost_equal( surf_eqn_disc.evaluate(None, linear_y), y_surf )
def test_processed_variable_1D_unknown_domain(self): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") geometry = pybamm.Geometry() geometry.add_domain( "SEI layer", { "primary": { x: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(1) } } }, ) submesh_types = {"SEI layer": pybamm.Uniform1DSubMesh} var_pts = {x: 100} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) nt = 100 solution = pybamm.Solution( np.linspace(0, 1, nt), np.zeros((var_pts[x], nt)), np.linspace(0, 1, 1), np.zeros((var_pts[x])), "test", ) c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] pybamm.ProcessedVariable(c, solution)
def default_geometry(self): var = pybamm.standard_spatial_vars geometry = { "current collector": { var.y: { "min": 0, "max": pybamm.geometric_parameters.l_y }, var.z: { "min": 0, "max": pybamm.geometric_parameters.l_z }, "tabs": { "negative": { "y_centre": pybamm.geometric_parameters.centre_y_tab_n, "z_centre": pybamm.geometric_parameters.centre_z_tab_n, "width": pybamm.geometric_parameters.l_tab_n, }, "positive": { "y_centre": pybamm.geometric_parameters.centre_y_tab_p, "z_centre": pybamm.geometric_parameters.centre_z_tab_p, "width": pybamm.geometric_parameters.l_tab_p, }, }, } } return pybamm.Geometry(geometry)
def get_1p1d_mesh_for_testing( xpts=None, zpts=15, cc_submesh=pybamm.MeshGenerator(pybamm.Uniform1DSubMesh) ): geometry = pybamm.Geometry("1+1D macro") return get_mesh_for_testing( xpts=xpts, zpts=zpts, geometry=geometry, cc_submesh=cc_submesh )
def default_geometry(self): geometry = {} var = pybamm.standard_spatial_vars if self.options["dimensionality"] == 1: geometry["current collector"] = { var.z: {"min": 0, "max": 1}, "tabs": { "negative": { "z_centre": pybamm.geometric_parameters.centre_z_tab_n }, "positive": { "z_centre": pybamm.geometric_parameters.centre_z_tab_p }, }, } elif self.options["dimensionality"] == 2: geometry["current collector"] = { var.y: {"min": 0, "max": pybamm.geometric_parameters.l_y}, var.z: {"min": 0, "max": pybamm.geometric_parameters.l_z}, "tabs": { "negative": { "y_centre": pybamm.geometric_parameters.centre_y_tab_n, "z_centre": pybamm.geometric_parameters.centre_z_tab_n, "width": pybamm.geometric_parameters.l_tab_n, }, "positive": { "y_centre": pybamm.geometric_parameters.centre_y_tab_p, "z_centre": pybamm.geometric_parameters.centre_z_tab_p, "width": pybamm.geometric_parameters.l_tab_p, }, }, } return pybamm.Geometry(geometry)
def get_2p1d_mesh_for_testing( xpts=None, ypts=15, zpts=15, cc_submesh=pybamm.MeshGenerator(pybamm.ScikitUniform2DSubMesh), ): geometry = pybamm.Geometry("2+1D macro") return get_mesh_for_testing( xpts=xpts, zpts=zpts, geometry=geometry, cc_submesh=cc_submesh )
def test_combine_geometries_3D(self): geometry3Dmacro = pybamm.Geometry3DMacro() geometry1Dmicro = pybamm.Geometry1DMicro() geometry = pybamm.Geometry(geometry3Dmacro, geometry1Dmicro) self.assertEqual( set(geometry.keys()), set([ "negative electrode", "separator", "positive electrode", "negative particle", "positive particle", "current collector", ]), ) with self.assertRaises(ValueError): geometry1Dmacro = pybamm.Geometry1DMacro() geometry = pybamm.Geometry(geometry3Dmacro, geometry1Dmacro)
def get_mesh_for_testing(xpts=None, rpts=10, ypts=15, zpts=15, geometry=None, cc_submesh=None): param = pybamm.ParameterValues( values={ "Electrode width [m]": 0.4, "Electrode height [m]": 0.5, "Negative tab width [m]": 0.1, "Negative tab centre y-coordinate [m]": 0.1, "Negative tab centre z-coordinate [m]": 0.0, "Positive tab width [m]": 0.1, "Positive tab centre y-coordinate [m]": 0.3, "Positive tab centre z-coordinate [m]": 0.5, "Negative electrode thickness [m]": 0.3, "Separator thickness [m]": 0.3, "Positive electrode thickness [m]": 0.3, }) if geometry is None: geometry = pybamm.Geometry("1D macro", "1D micro") param.process_geometry(geometry) submesh_types = { "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), "current collector": pybamm.MeshGenerator(pybamm.SubMesh0D), } if cc_submesh: submesh_types["current collector"] = cc_submesh if xpts is None: xn_pts, xs_pts, xp_pts = 40, 25, 35 else: xn_pts, xs_pts, xp_pts = xpts, xpts, xpts var = pybamm.standard_spatial_vars var_pts = { var.x_n: xn_pts, var.x_s: xs_pts, var.x_p: xp_pts, var.r_n: rpts, var.r_p: rpts, var.y: ypts, var.z: zpts, } return pybamm.Mesh(geometry, submesh_types, var_pts)
def test_multiple_meshes_macro(self): param = pybamm.ParameterValues( values={ "Negative electrode thickness [m]": 0.1, "Separator thickness [m]": 0.2, "Positive electrode thickness [m]": 0.3, "Electrode height [m]": 0.4, "Negative tab centre z-coordinate [m]": 0.0, "Positive tab centre z-coordinate [m]": 0.4, }) geometry = pybamm.Geometry("1+1D macro") param.process_geometry(geometry) # provide mesh properties var = pybamm.standard_spatial_vars var_pts = {var.x_n: 10, var.x_s: 15, var.x_p: 20, var.z: 5} submesh_types = { "negative electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "separator": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "current collector": pybamm.MeshGenerator(pybamm.SubMesh0D), } mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check types self.assertIsInstance(mesh["negative electrode"], list) self.assertIsInstance(mesh["separator"], list) self.assertIsInstance(mesh["positive electrode"], list) self.assertEqual(len(mesh["negative electrode"]), 5) self.assertEqual(len(mesh["separator"]), 5) self.assertEqual(len(mesh["positive electrode"]), 5) for i in range(5): self.assertIsInstance(mesh["negative electrode"][i], pybamm.Uniform1DSubMesh) self.assertIsInstance(mesh["separator"][i], pybamm.Uniform1DSubMesh) self.assertIsInstance(mesh["positive electrode"][i], pybamm.Uniform1DSubMesh) self.assertEqual(mesh["negative electrode"][i].npts, 10) self.assertEqual(mesh["separator"][i].npts, 15) self.assertEqual(mesh["positive electrode"][i].npts, 20)
def test_add_domain(self): L = pybamm.Parameter("L") zero = pybamm.Scalar(0) x = pybamm.SpatialVariable("x", domain=["negative electrode"]) geometry = pybamm.Geometry() geometry.add_domain("name_of_domain", {"primary": {x: {"min": zero, "max": L}}}) geometry.add_domain("name_of_domain", {"tabs": {"negative": {"z_centre": L}}}) # name must be a string with self.assertRaisesRegex(ValueError, "name must be a string"): geometry.add_domain(123, {"primary": {x: {"min": zero, "max": L}}}) # keys of geometry must be either \"primary\" or \"secondary\" with self.assertRaisesRegex(ValueError, "primary.*secondary"): geometry.add_domain( "name_of_domain", {"primaryy": {x: {"min": zero, "max": L}}} ) # inner dict of geometry must have pybamm.SpatialVariable as keys with self.assertRaisesRegex(ValueError, "pybamm\.SpatialVariable as keys"): geometry.add_domain( "name_of_domain", {"primary": {L: {"min": zero, "max": L}}} ) # no minimum extents for variable with self.assertRaisesRegex(ValueError, "minimum"): geometry.add_domain("name_of_domain", {"primary": {x: {"max": L}}}) # no maximum extents for variable with self.assertRaisesRegex(ValueError, "maximum"): geometry.add_domain("name_of_domain", {"primary": {x: {"min": zero}}}) # tabs region must be \"negative\" or \"positive\" with self.assertRaisesRegex(ValueError, "negative.*positive"): geometry.add_domain( "name_of_domain", {"tabs": {"negativee": {"z_centre": L}}} ) # tabs region params must be \"y_centre\", "\"z_centre\" or \"width\" with self.assertRaisesRegex(ValueError, "y_centre.*z_centre.*width"): geometry.add_domain( "name_of_domain", {"tabs": {"negative": {"z_centree": L}}} )
def test_read_parameters(self): L_n = pybamm.geometric_parameters.L_n L_s = pybamm.geometric_parameters.L_s L_p = pybamm.geometric_parameters.L_p L_y = pybamm.geometric_parameters.L_y L_z = pybamm.geometric_parameters.L_z tab_n_y = pybamm.geometric_parameters.Centre_y_tab_n tab_n_z = pybamm.geometric_parameters.Centre_z_tab_n L_tab_n = pybamm.geometric_parameters.L_tab_n tab_p_y = pybamm.geometric_parameters.Centre_y_tab_p tab_p_z = pybamm.geometric_parameters.Centre_z_tab_p L_tab_p = pybamm.geometric_parameters.L_tab_p geometry = pybamm.Geometry("2+1D macro", "(2+1)+1D micro") self.assertEqual( set([x.name for x in geometry.parameters]), set( [ x.name for x in [ L_n, L_s, L_p, L_y, L_z, tab_n_y, tab_n_z, L_tab_n, tab_p_y, tab_p_z, L_tab_p, ] ] ), ) self.assertTrue( all(isinstance(x, pybamm.Parameter) for x in geometry.parameters) )
def test_multiple_meshes(self): param = pybamm.ParameterValues( values={ "Negative electrode thickness [m]": 0.1, "Separator thickness [m]": 0.2, "Positive electrode thickness [m]": 0.3, }) geometry = pybamm.Geometry("1+1D micro") param.process_geometry(geometry) # provide mesh properties var = pybamm.standard_spatial_vars var_pts = {var.x_n: 10, var.x_p: 10, var.r_n: 5, var.r_p: 6} submesh_types = { "negative particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "positive particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "current collector": pybamm.MeshGenerator(pybamm.SubMesh0D), } mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check types self.assertIsInstance(mesh["negative particle"], list) self.assertIsInstance(mesh["positive particle"], list) self.assertEqual(len(mesh["negative particle"]), 10) self.assertEqual(len(mesh["positive particle"]), 10) for i in range(10): self.assertIsInstance(mesh["negative particle"][i], pybamm.Uniform1DSubMesh) self.assertIsInstance(mesh["positive particle"][i], pybamm.Uniform1DSubMesh) self.assertEqual(mesh["negative particle"][i].npts, 5) self.assertEqual(mesh["positive particle"][i].npts, 6)
"positive interface current"] = pybamm.interface.CurrentForInverseButlerVolmer( model.param, "Positive", "lithium-ion main") model.submodels[ "electrolyte diffusion"] = pybamm.electrolyte_diffusion.ConstantConcentration( model.param) model.submodels[ "electrolyte conductivity"] = pybamm.electrolyte_conductivity.LeadingOrder( model.param) model.submodels["negative sei"] = pybamm.sei.NoSEI(model.param, "Negative") model.submodels["positive sei"] = pybamm.sei.NoSEI(model.param, "Positive") # build model model.build_model() # create geometry geometry = pybamm.Geometry("1D macro", "1D micro") # process model and geometry param = model.default_parameter_values param.process_model(model) param.process_geometry(geometry) # set mesh # Note: li-ion base model has defaults for mesh and var_pts mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) # discretise model # Note: li-ion base model has default spatial methods disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model)
def battery_geometry(include_particles=True, current_collector_dimension=0): """ A convenience function to create battery geometries. Parameters ---------- include_particles : bool Whether to include particle domains current_collector_dimensions : int, default The dimensions of the current collector. Should be 0 (default), 1 or 2 Returns ------- :class:`pybamm.Geometry` A geometry class for the battery """ var = pybamm.standard_spatial_vars geo = pybamm.geometric_parameters l_n = geo.l_n l_s = geo.l_s geometry = { "negative electrode": { var.x_n: { "min": 0, "max": l_n } }, "separator": { var.x_s: { "min": l_n, "max": l_n + l_s } }, "positive electrode": { var.x_p: { "min": l_n + l_s, "max": 1 } }, } # Add particle domains if include_particles is True: geometry.update({ "negative particle": { var.r_n: { "min": 0, "max": 1 } }, "positive particle": { var.r_p: { "min": 0, "max": 1 } }, }) if current_collector_dimension == 0: geometry["current collector"] = {var.z: {"position": 1}} elif current_collector_dimension == 1: geometry["current collector"] = { var.z: { "min": 0, "max": 1 }, "tabs": { "negative": { "z_centre": geo.centre_z_tab_n }, "positive": { "z_centre": geo.centre_z_tab_p }, }, } elif current_collector_dimension == 2: geometry["current collector"] = { var.y: { "min": 0, "max": geo.l_y }, var.z: { "min": 0, "max": geo.l_z }, "tabs": { "negative": { "y_centre": geo.centre_y_tab_n, "z_centre": geo.centre_z_tab_n, "width": geo.l_tab_n, }, "positive": { "y_centre": geo.centre_y_tab_p, "z_centre": geo.centre_z_tab_p, "width": geo.l_tab_p, }, }, } else: raise pybamm.GeometryError( "Invalid current collector dimension '{}' (should be 0, 1 or 2)". format(current_collector_dimension)) return pybamm.Geometry(geometry)
def default_geometry(self): return pybamm.Geometry("2D current collector")
def get_p2d_mesh_for_testing(xpts=None, rpts=10): geometry = pybamm.Geometry("1D macro", "1+1D micro") return get_mesh_for_testing(xpts=xpts, rpts=rpts, geometry=geometry)
def default_geometry(self): return pybamm.Geometry("1D macro", "1+1D micro")
def half_cell_geometry( include_particles=True, current_collector_dimension=0, working_electrode="positive" ): """ A convenience function to create battery geometries. Parameters ---------- include_particles : bool Whether to include particle domains current_collector_dimensions : int, default The dimensions of the current collector. Should be 0 (default), 1 or 2 current_collector_dimensions : string The electrode taking as working electrode. Should be "positive" or "negative" Returns ------- :class:`pybamm.Geometry` A geometry class for the battery """ var = half_cell_spatial_vars geo = pybamm.geometric_parameters if working_electrode == "positive": l_w = geo.l_p elif working_electrode == "negative": l_w = geo.l_n else: raise ValueError( "The option 'working_electrode' should be either 'positive'" " or 'negative'" ) l_Li = geo.l_Li l_s = geo.l_s geometry = { "lithium counter electrode": {var.x_Li: {"min": 0, "max": l_Li}}, "separator": {var.x_s: {"min": l_Li, "max": l_Li + l_s}}, "working electrode": {var.x_w: {"min": l_Li + l_s, "max": l_Li + l_s + l_w}}, } # Add particle domains if include_particles is True: geometry.update({"working particle": {var.r_w: {"min": 0, "max": 1}}}) if current_collector_dimension == 0: geometry["current collector"] = {var.z: {"position": 1}} elif current_collector_dimension == 1: geometry["current collector"] = { var.z: {"min": 0, "max": 1}, "tabs": { "negative": {"z_centre": geo.centre_z_tab_n}, "positive": {"z_centre": geo.centre_z_tab_p}, }, } elif current_collector_dimension == 2: geometry["current collector"] = { var.y: {"min": 0, "max": geo.l_y}, var.z: {"min": 0, "max": geo.l_z}, "tabs": { "negative": { "y_centre": geo.centre_y_tab_n, "z_centre": geo.centre_z_tab_n, "width": geo.l_tab_n, }, "positive": { "y_centre": geo.centre_y_tab_p, "z_centre": geo.centre_z_tab_p, "width": geo.l_tab_p, }, }, } else: raise pybamm.GeometryError( "Invalid current collector dimension '{}' (should be 0, 1 or 2)".format( current_collector_dimension ) ) return pybamm.Geometry(geometry)
def __init__(self, param=None): # Set fixed parameters here if param is None: param = pybamm.ParameterValues({ "Far-field concentration of S(soln) [mol cm-3]": 1e-6, "Diffusion Constant [cm2 s-1]": 7.2e-6, "Faraday Constant [C mol-1]": 96485.3328959, "Gas constant [J K-1 mol-1]": 8.314459848, "Electrode Area [cm2]": 0.07, "Temperature [K]": 273.0, "Voltage frequency [rad s-1]": 9.0152, "Voltage start [V]": 0.4, "Voltage reverse [V]": -0.4, "Voltage amplitude [V]": 0.0, # 0.05, "Scan Rate [V s-1]": 0.05, "Electrode Coverage [mol cm2]": 6.5e-12, }) # Create dimensional fixed parameters D = pybamm.Parameter("Diffusion Constant [cm2 s-1]") F = pybamm.Parameter("Faraday Constant [C mol-1]") R = pybamm.Parameter("Gas constant [J K-1 mol-1]") a = pybamm.Parameter("Electrode Area [cm2]") T = pybamm.Parameter("Temperature [K]") omega_d = pybamm.Parameter("Voltage frequency [rad s-1]") E_start_d = pybamm.Parameter("Voltage start [V]") E_reverse_d = pybamm.Parameter("Voltage reverse [V]") deltaE_d = pybamm.Parameter("Voltage amplitude [V]") v = pybamm.Parameter("Scan Rate [V s-1]") Gamma = pybamm.Parameter("Electrode Coverage [mol cm2]") # Create dimensional input parameters E0_d = pybamm.InputParameter("Reversible Potential [V]") k0_d = pybamm.InputParameter("Redox Rate [s-1]") kcat_d = pybamm.InputParameter("Catalytic Rate [cm3 mol-l s-1]") alpha = pybamm.InputParameter("Symmetry factor [non-dim]") Cdl_d = pybamm.InputParameter("Capacitance [F]") Ru_d = pybamm.InputParameter("Uncompensated Resistance [Ohm]") # Create scaling factors for non-dimensionalisation E_0 = R * T / F T_0 = E_0 / v I_0 = F * a * Gamma / T L_0 = pybamm.sqrt(D * T_0) # Non-dimensionalise parameters E0 = E0_d / E_0 k0 = k0_d * T_0 kcat = kcat_d * Gamma * L_0 / D Cdl = Cdl_d * a * E_0 / (I_0 * T_0) Ru = Ru_d * I_0 / E_0 omega = 2 * np.pi * omega_d * T_0 E_start = E_start_d / E_0 E_reverse = E_reverse_d / E_0 t_reverse = E_start - E_reverse deltaE = deltaE_d / E_0 # Input voltage protocol Edc_forward = -pybamm.t Edc_backwards = pybamm.t - 2 * t_reverse Eapp = E_start + \ (pybamm.t <= t_reverse) * Edc_forward + \ (pybamm.t > t_reverse) * Edc_backwards + \ deltaE * pybamm.sin(omega * pybamm.t) # create PyBaMM model object model = pybamm.BaseModel() # Create state variables for model theta = pybamm.Variable("O(surf) [non-dim]") c = pybamm.Variable("S(soln) [non-dim]", domain="solution") i = pybamm.Variable("Current [non-dim]") # Effective potential Eeff = Eapp - i * Ru # Faridaic current (Butler Volmer) i_f = k0 * ((1 - theta) * pybamm.exp( (1 - alpha) * (Eeff - E0)) - theta * pybamm.exp(-alpha * (Eeff - E0))) c_at_electrode = pybamm.BoundaryValue(c, "left") # Catalytic current i_cat = kcat * c_at_electrode * (1 - theta) # ODE equations model.rhs = { theta: i_f + i_cat, i: 1 / (Cdl * Ru) * (i_f + Cdl * Eapp.diff(pybamm.t) - i - i_cat), c: pybamm.div(pybamm.grad(c)), } # algebraic equations (none) model.algebraic = {} # Boundary and initial conditions model.boundary_conditions = { c: { "right": (pybamm.Scalar(1), "Dirichlet"), "left": (i_cat, "Neumann"), } } model.initial_conditions = { theta: pybamm.Scalar(1), i: Cdl * Eapp.diff(pybamm.t), c: pybamm.Scalar(1), } # set spatial variables and solution domain geometry x = pybamm.SpatialVariable('x', domain="solution") model.geometry = pybamm.Geometry({ "solution": { x: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(20) } } }) model.var_pts = {x: 100} # Using Finite Volume discretisation on an expanding 1D grid model.submesh_types = { "solution": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh, {'side': 'left'}) } model.spatial_methods = {"solution": pybamm.FiniteVolume()} # model variables model.variables = { "Current [non-dim]": i, "O(surf) [non-dim]": theta, "S(soln) at electrode [non-dim]": c_at_electrode, "Applied Voltage [non-dim]": Eapp, } # -------------------------------- # Set model parameters param.process_model(model) geometry = model.geometry param.process_geometry(geometry) # Create mesh and discretise model mesh = pybamm.Mesh(geometry, model.submesh_types, model.var_pts) disc = pybamm.Discretisation(mesh, model.spatial_methods) disc.process_model(model) # Create solver model.convert_to_format = 'python' solver = pybamm.ScipySolver(method='BDF', rtol=1e-6, atol=1e-6) #solver = pybamm.CasadiSolver(mode='fast', rtol=1e-8, atol=1e-10) # Store discretised model and solver self._model = model self._param = param self._solver = solver self._omega_d = param.process_symbol(omega_d).evaluate() self._I_0 = param.process_symbol(I_0).evaluate() self._T_0 = param.process_symbol(T_0).evaluate()
def test_combine_submeshes(self): param = pybamm.ParameterValues( values={ "Negative electrode thickness [m]": 0.1, "Separator thickness [m]": 0.2, "Positive electrode thickness [m]": 0.3, }) geometry = pybamm.Geometry("1D macro", "1D micro") param.process_geometry(geometry) # provide mesh properties var = pybamm.standard_spatial_vars var_pts = { var.x_n: 10, var.x_s: 10, var.x_p: 12, var.r_n: 5, var.r_p: 6 } submesh_types = { "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), "current collector": pybamm.MeshGenerator(pybamm.SubMesh0D), } mesh_type = pybamm.Mesh # create mesh mesh = mesh_type(geometry, submesh_types, var_pts) # create submesh submesh = mesh.combine_submeshes("negative electrode", "separator") self.assertEqual(submesh[0].edges[0], 0) self.assertEqual(submesh[0].edges[-1], mesh["separator"][0].edges[-1]) np.testing.assert_almost_equal( submesh[0].nodes - np.concatenate([ mesh["negative electrode"][0].nodes, mesh["separator"][0].nodes ]), 0, ) np.testing.assert_almost_equal(submesh[0].internal_boundaries, [0.1 / 0.6]) with self.assertRaises(pybamm.DomainError): mesh.combine_submeshes("negative electrode", "positive electrode") # test errors geometry = { "negative electrode": { "primary": { var.x_n: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(0.5) } } }, "negative particle": { "primary": { var.r_n: { "min": pybamm.Scalar(0.5), "max": pybamm.Scalar(1) } } }, } param.process_geometry(geometry) mesh = pybamm.Mesh(geometry, submesh_types, var_pts) with self.assertRaisesRegex(pybamm.DomainError, "trying"): mesh.combine_submeshes("negative electrode", "negative particle") with self.assertRaisesRegex( ValueError, "Submesh domains being combined cannot be empty"): mesh.combine_submeshes()
def test_1D_different_domains(self): # Negative electrode domain var = pybamm.Variable("var", domain=["negative electrode"]) x = pybamm.SpatialVariable("x", domain=["negative electrode"]) disc = tests.get_discretisation_for_testing() disc.set_variable_slices([var]) x_sol = disc.process_symbol(x).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # Particle domain var = pybamm.Variable("var", domain=["negative particle"]) r = pybamm.SpatialVariable("r", domain=["negative particle"]) disc = tests.get_discretisation_for_testing() disc.set_variable_slices([var]) r_sol = disc.process_symbol(r).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(r_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # Current collector domain var = pybamm.Variable("var", domain=["current collector"]) 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] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(z_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # Other domain var = pybamm.Variable("var", domain=["line"]) x = pybamm.SpatialVariable("x", domain=["line"]) geometry = pybamm.Geometry( {"line": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}} ) submesh_types = {"line": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)} var_pts = {x: 10} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) disc = pybamm.Discretisation(mesh, {"line": pybamm.FiniteVolume()}) disc.set_variable_slices([var]) x_sol = disc.process_symbol(x).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # 2D fails var = pybamm.Variable( "var", domain=["negative particle"], auxiliary_domains={"secondary": "negative electrode"}, ) r = pybamm.SpatialVariable( "r", domain=["negative particle"], auxiliary_domains={"secondary": "negative electrode"}, ) disc = tests.get_p2d_discretisation_for_testing() disc.set_variable_slices([var]) r_sol = disc.process_symbol(r).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(r_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): pybamm.ProcessedSymbolicVariable(var_sol, sol)
def __init__(self, n=100, max_x=10, param=None): # Set fixed parameters here if param is None: param = pybamm.ParameterValues({ "Far-field concentration of A [mol cm-3]": 1e-6, "Diffusion Constant [cm2 s-1]": 7.2e-6, "Faraday Constant [C mol-1]": 96485.3328959, "Gas constant [J K-1 mol-1]": 8.314459848, "Electrode Area [cm2]": 0.07, "Temperature [K]": 297.0, "Voltage frequency [rad s-1]": 9.0152, "Voltage start [V]": 0.5, "Voltage reverse [V]": -0.1, "Voltage amplitude [V]": 0.08, "Scan Rate [V s-1]": 0.08941, }) # Create dimensional fixed parameters c_inf = pybamm.Parameter("Far-field concentration of A [mol cm-3]") D = pybamm.Parameter("Diffusion Constant [cm2 s-1]") F = pybamm.Parameter("Faraday Constant [C mol-1]") R = pybamm.Parameter("Gas constant [J K-1 mol-1]") S = pybamm.Parameter("Electrode Area [cm2]") T = pybamm.Parameter("Temperature [K]") E_start_d = pybamm.Parameter("Voltage start [V]") E_reverse_d = pybamm.Parameter("Voltage reverse [V]") deltaE_d = pybamm.Parameter("Voltage amplitude [V]") v = pybamm.Parameter("Scan Rate [V s-1]") # Create dimensional input parameters E0 = pybamm.InputParameter("Reversible Potential [non-dim]") k0 = pybamm.InputParameter("Reaction Rate [non-dim]") alpha = pybamm.InputParameter("Symmetry factor [non-dim]") Cdl = pybamm.InputParameter("Capacitance [non-dim]") Ru = pybamm.InputParameter("Uncompensated Resistance [non-dim]") omega_d = pybamm.InputParameter("Voltage frequency [rad s-1]") E0_d = pybamm.InputParameter("Reversible Potential [V]") k0_d = pybamm.InputParameter("Reaction Rate [s-1]") alpha = pybamm.InputParameter("Symmetry factor [non-dim]") Cdl_d = pybamm.InputParameter("Capacitance [F]") Ru_d = pybamm.InputParameter("Uncompensated Resistance [Ohm]") # Create scaling factors for non-dimensionalisation E_0 = R * T / F T_0 = E_0 / v L_0 = pybamm.sqrt(D * T_0) I_0 = D * F * S * c_inf / L_0 # Non-dimensionalise parameters E0 = E0_d / E_0 k0 = k0_d * L_0 / D Cdl = Cdl_d * S * E_0 / (I_0 * T_0) Ru = Ru_d * I_0 / E_0 omega = 2 * np.pi * omega_d * T_0 E_start = E_start_d / E_0 E_reverse = E_reverse_d / E_0 t_reverse = E_start - E_reverse deltaE = deltaE_d / E_0 # Input voltage protocol Edc_forward = -pybamm.t Edc_backwards = pybamm.t - 2 * t_reverse Eapp = E_start + \ (pybamm.t <= t_reverse) * Edc_forward + \ (pybamm.t > t_reverse) * Edc_backwards + \ deltaE * pybamm.sin(omega * pybamm.t) # create PyBaMM model object model = pybamm.BaseModel() # Create state variables for model theta = pybamm.Variable("ratio_A", domain="solution") i = pybamm.Variable("Current") # Effective potential Eeff = Eapp - i * Ru # Faradaic current i_f = pybamm.BoundaryGradient(theta, "left") # ODE equations model.rhs = { theta: pybamm.div(pybamm.grad(theta)), i: 1 / (Cdl * Ru) * (-i_f + Cdl * Eapp.diff(pybamm.t) - i), } # algebraic equations (none) model.algebraic = {} # Butler-volmer boundary condition at electrode theta_at_electrode = pybamm.BoundaryValue(theta, "left") butler_volmer = k0 * (theta_at_electrode * pybamm.exp(-alpha * (Eeff - E0)) - (1 - theta_at_electrode) * pybamm.exp( (1 - alpha) * (Eeff - E0))) # Boundary and initial conditions model.boundary_conditions = { theta: { "right": (pybamm.Scalar(1), "Dirichlet"), "left": (butler_volmer, "Neumann"), } } model.initial_conditions = { theta: pybamm.Scalar(1), i: Cdl * (-1.0 + deltaE * omega), } # set spatial variables and solution domain geometry x = pybamm.SpatialVariable('x', domain="solution") default_geometry = pybamm.Geometry({ "solution": { x: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(max_x) } } }) default_var_pts = {x: n} # Using Finite Volume discretisation on an expanding 1D grid for solution default_submesh_types = { "solution": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh, {'side': 'left'}) } default_spatial_methods = {"solution": pybamm.FiniteVolume()} # model variables model.variables = { "Current [non-dim]": i, } #-------------------------------- # Set model parameters param.process_model(model) geometry = default_geometry param.process_geometry(geometry) # Create mesh and discretise model mesh = pybamm.Mesh(geometry, default_submesh_types, default_var_pts) disc = pybamm.Discretisation(mesh, default_spatial_methods) disc.process_model(model) # Create solver solver = pybamm.CasadiSolver( mode="fast", rtol=1e-9, atol=1e-9, extra_options_setup={'print_stats': False}) #model.convert_to_format = 'jax' #solver = pybamm.JaxSolver(method='BDF') #model.convert_to_format = 'python' #solver = pybamm.ScipySolver(method='BDF') # Store discretised model and solver self._model = model self._solver = solver self._fast_solver = None self._omega_d = param["Voltage frequency [rad s-1]"] self._I_0 = param.process_symbol(I_0).evaluate() self._T_0 = param.process_symbol(T_0).evaluate() self._E_0 = param.process_symbol(E_0).evaluate() self._L_0 = param.process_symbol(L_0).evaluate() self._S = param.process_symbol(S).evaluate() self._D = param.process_symbol(D).evaluate() self._default_var_points = default_var_pts