def test_secondary_broadcast_2D(self): # secondary broadcast in 2D --> Matrix multiplication disc = get_discretisation_for_testing() mesh = disc.mesh var = pybamm.Variable("var", domain=["negative particle"]) broad = pybamm.SecondaryBroadcast(var, "negative electrode") disc.set_variable_slices([var]) broad_disc = disc.process_symbol(broad) self.assertIsInstance(broad_disc, pybamm.MatrixMultiplication) self.assertIsInstance(broad_disc.children[0], pybamm.Matrix) self.assertIsInstance(broad_disc.children[1], pybamm.StateVector) self.assertEqual( broad_disc.shape, (mesh["negative particle"].npts * mesh["negative electrode"].npts, 1), ) broad = pybamm.SecondaryBroadcast(var, "negative electrode") # test broadcast to edges broad_to_edges = pybamm.SecondaryBroadcastToEdges(var, "negative electrode") disc.set_variable_slices([var]) broad_to_edges_disc = disc.process_symbol(broad_to_edges) self.assertIsInstance(broad_to_edges_disc, pybamm.MatrixMultiplication) self.assertIsInstance(broad_to_edges_disc.children[0], pybamm.Matrix) self.assertIsInstance(broad_to_edges_disc.children[1], pybamm.StateVector) self.assertEqual( broad_to_edges_disc.shape, ( mesh["negative particle"].npts * (mesh["negative electrode"].npts + 1), 1, ), )
def test_secondary_broadcast(self): a = pybamm.Symbol( "a", domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.SecondaryBroadcast(a, ["negative electrode"]) self.assertEqual(broad_a.domain, ["negative particle"]) self.assertEqual( broad_a.auxiliary_domains, { "secondary": ["negative electrode"], "tertiary": ["current collector"] }, ) a = pybamm.Symbol("a", domain="negative particle") with self.assertRaisesRegex(pybamm.DomainError, "Secondary broadcast from particle"): pybamm.SecondaryBroadcast(a, "current collector") a = pybamm.Symbol("a", domain="negative electrode") with self.assertRaisesRegex(pybamm.DomainError, "Secondary broadcast from electrode"): pybamm.SecondaryBroadcast(a, "negative particle") a = pybamm.Symbol("a", domain="current collector") with self.assertRaisesRegex(pybamm.DomainError, "Cannot do secondary broadcast"): pybamm.SecondaryBroadcast(a, "electrode")
def test_processed_var_2D_secondary_broadcast(self): var = pybamm.Variable("var", domain=["negative particle"]) broad_var = pybamm.SecondaryBroadcast(var, "negative electrode") x = pybamm.SpatialVariable("x", domain=["negative electrode"]) r = pybamm.SpatialVariable("r", domain=["negative particle"]) disc = tests.get_discretisation_for_testing() disc.set_variable_slices([var]) x_sol = disc.process_symbol(x).entries[:, 0] r_sol = disc.process_symbol(r).entries[:, 0] var_sol = disc.process_symbol(broad_var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) processed_var = pybamm.ProcessedVariable( var_sol, pybamm.Solution(t_sol, y_sol), warn=False ) # 3 vectors np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol).shape, (10, 40, 50) ) np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol), np.reshape(y_sol, [len(r_sol), len(x_sol), len(t_sol)]), ) # 2 vectors, 1 scalar np.testing.assert_array_equal(processed_var(0.5, x_sol, r_sol).shape, (10, 40)) np.testing.assert_array_equal(processed_var(t_sol, 0.2, r_sol).shape, (10, 50)) np.testing.assert_array_equal(processed_var(t_sol, x_sol, 0.5).shape, (40, 50)) # 1 vectors, 2 scalar np.testing.assert_array_equal(processed_var(0.5, 0.2, r_sol).shape, (10,)) np.testing.assert_array_equal(processed_var(0.5, x_sol, 0.5).shape, (40,)) np.testing.assert_array_equal(processed_var(t_sol, 0.2, 0.5).shape, (50,)) # 3 scalars np.testing.assert_array_equal(processed_var(0.2, 0.2, 0.2).shape, ()) # positive particle var = pybamm.Variable("var", domain=["positive particle"]) broad_var = pybamm.SecondaryBroadcast(var, "positive electrode") x = pybamm.SpatialVariable("x", domain=["positive electrode"]) r = pybamm.SpatialVariable("r", domain=["positive particle"]) disc.set_variable_slices([var]) x_sol = disc.process_symbol(x).entries[:, 0] r_sol = disc.process_symbol(r).entries[:, 0] var_sol = disc.process_symbol(broad_var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) processed_var = pybamm.ProcessedVariable( var_sol, pybamm.Solution(t_sol, y_sol), warn=False ) # 3 vectors np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) )
def get_fundamental_variables(self): if self.domain == "Negative": c_s_xav = pybamm.standard_variables.c_s_n_xav c_s = pybamm.SecondaryBroadcast(c_s_xav, ["negative electrode"]) elif self.domain == "Positive": c_s_xav = pybamm.standard_variables.c_s_p_xav c_s = pybamm.SecondaryBroadcast(c_s_xav, ["positive electrode"]) variables = self._get_standard_concentration_variables(c_s, c_s_xav) return variables
def test_r_average(self): a = pybamm.Scalar(1) average_a = pybamm.r_average(a) self.assertEqual(average_a.id, a.id) average_broad_a = pybamm.r_average( pybamm.PrimaryBroadcast(a, ["negative particle"]) ) self.assertEqual(average_broad_a.evaluate(), np.array([1])) for domain in [["negative particle"], ["positive particle"]]: a = pybamm.Symbol("a", domain=domain) r = pybamm.SpatialVariable("r", domain) av_a = pybamm.r_average(a) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, r.domain) # electrode domains go to current collector when averaged self.assertEqual(av_a.domain, []) # r-average of a symbol that is broadcast to x # takes the average of the child then broadcasts it a = pybamm.Scalar(1, domain="positive particle") broad_a = pybamm.SecondaryBroadcast(a, "positive electrode") average_broad_a = pybamm.r_average(broad_a) self.assertIsInstance(average_broad_a, pybamm.PrimaryBroadcast) self.assertEqual(average_broad_a.domain, ["positive electrode"]) self.assertEqual(average_broad_a.children[0].id, pybamm.r_average(a).id) # 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)
def get_fundamental_variables(self): # The particle concentration is uniform throughout the particle, so we # can just use the surface value. This avoids dealing with both # x *and* r averaged quantities, which may be confusing. if self.domain == "Negative": c_s_surf_xav = pybamm.standard_variables.c_s_n_surf_xav c_s_xav = pybamm.PrimaryBroadcast(c_s_surf_xav, ["negative particle"]) c_s = pybamm.SecondaryBroadcast(c_s_xav, ["negative electrode"]) N_s = pybamm.FullBroadcastToEdges( 0, ["negative particle"], auxiliary_domains={ "secondary": "negative electrode", "tertiary": "current collector", }, ) N_s_xav = pybamm.FullBroadcast(0, "negative electrode", "current collector") elif self.domain == "Positive": c_s_surf_xav = pybamm.standard_variables.c_s_p_surf_xav c_s_xav = pybamm.PrimaryBroadcast(c_s_surf_xav, ["positive particle"]) c_s = pybamm.SecondaryBroadcast(c_s_xav, ["positive electrode"]) N_s = pybamm.FullBroadcastToEdges( 0, ["positive particle"], auxiliary_domains={ "secondary": "positive electrode", "tertiary": "current collector", }, ) N_s_xav = pybamm.FullBroadcast(0, "positive electrode", "current collector") variables = 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 test_symbol_new_copy(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") v_n = pybamm.Variable("v", "negative electrode") x_n = pybamm.standard_spatial_vars.x_n v_s = pybamm.Variable("v", "separator") vec = pybamm.Vector([1, 2, 3, 4, 5]) mat = pybamm.Matrix([[1, 2], [3, 4]]) mesh = get_mesh_for_testing() for symbol in [ a + b, a - b, a * b, a / b, a**b, -a, abs(a), pybamm.Function(np.sin, a), pybamm.FunctionParameter("function", {"a": a}), pybamm.grad(v_n), pybamm.div(pybamm.grad(v_n)), pybamm.upwind(v_n), pybamm.IndefiniteIntegral(v_n, x_n), pybamm.BackwardIndefiniteIntegral(v_n, x_n), pybamm.BoundaryValue(v_n, "right"), pybamm.BoundaryGradient(v_n, "right"), pybamm.PrimaryBroadcast(a, "domain"), pybamm.SecondaryBroadcast(v_n, "current collector"), pybamm.FullBroadcast(a, "domain", {"secondary": "other domain"}), pybamm.concatenation(v_n, v_s), pybamm.NumpyConcatenation(a, b, v_s), pybamm.DomainConcatenation([v_n, v_s], mesh), pybamm.Parameter("param"), pybamm.InputParameter("param"), pybamm.StateVector(slice(0, 56)), pybamm.Matrix(np.ones((50, 40))), pybamm.SpatialVariable("x", ["negative electrode"]), pybamm.t, pybamm.Index(vec, 1), pybamm.NotConstant(a), pybamm.ExternalVariable( "external variable", 20, domain="test", auxiliary_domains={"secondary": "test2"}, ), pybamm.minimum(a, b), pybamm.maximum(a, b), pybamm.SparseStack(mat, mat), ]: self.assertEqual(symbol.id, symbol.new_copy().id)
def test_secondary_broadcast(self): a = pybamm.Symbol( "a", domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.SecondaryBroadcast(a, ["negative electrode"]) self.assertEqual(broad_a.domain, ["negative particle"]) self.assertEqual( broad_a.auxiliary_domains, { "secondary": ["negative electrode"], "tertiary": ["current collector"] }, ) self.assertTrue(broad_a.broadcasts_to_nodes) with self.assertRaises(NotImplementedError): broad_a.reduce_one_dimension() a = pybamm.Symbol("a") with self.assertRaisesRegex(TypeError, "empty domain"): pybamm.SecondaryBroadcast(a, "current collector") a = pybamm.Symbol("a", domain="negative particle") with self.assertRaisesRegex(pybamm.DomainError, "Secondary broadcast from particle"): pybamm.SecondaryBroadcast(a, "current collector") a = pybamm.Symbol("a", domain="negative electrode") with self.assertRaisesRegex(pybamm.DomainError, "Secondary broadcast from electrode"): pybamm.SecondaryBroadcast(a, "negative particle") a = pybamm.Symbol("a", domain="current collector") with self.assertRaisesRegex(pybamm.DomainError, "Cannot do secondary broadcast"): pybamm.SecondaryBroadcast(a, "electrode")
def test_symbol_new_copy(self): a = pybamm.Scalar(0) b = pybamm.Scalar(1) v_n = pybamm.Variable("v", "negative electrode") x_n = pybamm.standard_spatial_vars.x_n v_s = pybamm.Variable("v", "separator") vec = pybamm.Vector(np.array([1, 2, 3, 4, 5])) mesh = get_mesh_for_testing() for symbol in [ a + b, a - b, a * b, a / b, a**b, -a, abs(a), pybamm.Function(np.sin, a), pybamm.FunctionParameter("function", {"a": a}), pybamm.grad(v_n), pybamm.div(pybamm.grad(v_n)), pybamm.Integral(a, pybamm.t), pybamm.IndefiniteIntegral(v_n, x_n), pybamm.BackwardIndefiniteIntegral(v_n, x_n), pybamm.BoundaryValue(v_n, "right"), pybamm.BoundaryGradient(v_n, "right"), pybamm.PrimaryBroadcast(a, "domain"), pybamm.SecondaryBroadcast(v_n, "current collector"), pybamm.FullBroadcast(a, "domain", {"secondary": "other domain"}), pybamm.Concatenation(v_n, v_s), pybamm.NumpyConcatenation(a, b, v_s), pybamm.DomainConcatenation([v_n, v_s], mesh), pybamm.Parameter("param"), pybamm.InputParameter("param"), pybamm.StateVector(slice(0, 56)), pybamm.Matrix(np.ones((50, 40))), pybamm.SpatialVariable("x", ["negative electrode"]), pybamm.t, pybamm.Index(vec, 1), ]: self.assertEqual(symbol.id, symbol.new_copy().id)
def get_coupled_variables(self, variables): c_s_xav = variables["X-averaged " + self.domain.lower() + " particle concentration"] T_xav = pybamm.PrimaryBroadcast( variables["X-averaged " + self.domain.lower() + " electrode temperature"], [self.domain.lower() + " particle"], ) if self.domain == "Negative": N_s_xav = -self.param.D_n(c_s_xav, T_xav) * pybamm.grad(c_s_xav) elif self.domain == "Positive": N_s_xav = -self.param.D_p(c_s_xav, T_xav) * pybamm.grad(c_s_xav) N_s = pybamm.SecondaryBroadcast(N_s_xav, [self._domain.lower() + " electrode"]) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) return variables
def get_coupled_variables(self, variables): c_s_rxav = variables["Average " + self.domain.lower() + " particle concentration"] i_boundary_cc = variables["Current collector current density"] T_xav = pybamm.PrimaryBroadcast( variables["X-averaged " + self.domain.lower() + " electrode temperature"], [self.domain.lower() + " particle"], ) # Set surface concentration based on polynomial order if self.name == "uniform profile": # The concentration is uniform so the surface value is equal to # the average c_s_surf_xav = c_s_rxav elif self.name == "quadratic profile": # The surface concentration is computed from the average concentration # and boundary flux # Note 1: here we use the total average interfacial current for the single # particle. We explicitly write this as the current density divided by the # electrode thickness instead of getting the average current from the # interface submodel since the interface submodel requires the surface # concentration to be defined first to compute the exchange current density. # Explicitly writing out the average interfacial current here avoids # KeyErrors due to variables not being set in the "right" order. # Note 2: the concentration, c, inside the diffusion coefficient, D, here # should really be the surface value, but this requires solving a nonlinear # equation for c_surf (if the diffusion coefficient is nonlinear), adding # an extra algebraic equation to solve. For now, using the average c is an # ok approximation and means the SPM(e) still gives a system of ODEs rather # than DAEs. if self.domain == "Negative": j_xav = i_boundary_cc / self.param.l_n c_s_surf_xav = c_s_rxav - self.param.C_n * ( j_xav / 5 / self.param.a_R_n / self.param.D_n(c_s_rxav, pybamm.surf(T_xav))) if self.domain == "Positive": j_xav = -i_boundary_cc / self.param.l_p c_s_surf_xav = c_s_rxav - self.param.C_p * ( j_xav / 5 / self.param.a_R_p / self.param.gamma_p / self.param.D_p(c_s_rxav, pybamm.surf(T_xav))) elif self.name == "quartic profile": # The surface concentration is computed from the average concentration, # the average concentration gradient and the boundary flux (see notes # for the case order=2) q_s_rxav = variables["Average " + self.domain.lower() + " particle concentration gradient"] if self.domain == "Negative": j_xav = i_boundary_cc / self.param.l_n c_s_surf_xav = (c_s_rxav + 8 * q_s_rxav / 35 - self.param.C_n * (j_xav / 35 / self.param.a_R_n / self.param.D_n(c_s_rxav, pybamm.surf(T_xav)))) if self.domain == "Positive": j_xav = -i_boundary_cc / self.param.l_p c_s_surf_xav = ( c_s_rxav + 8 * q_s_rxav / 35 - self.param.C_p * (j_xav / 35 / self.param.a_R_p / self.param.gamma_p / self.param.D_p(c_s_rxav, pybamm.surf(T_xav)))) # Set concentration depending on polynomial order if self.name == "uniform profile": # The concentration is uniform c_s_xav = pybamm.PrimaryBroadcast( c_s_rxav, [self.domain.lower() + " particle"]) elif self.name == "quadratic profile": # The concentration is given by c = A + B*r**2 A = pybamm.PrimaryBroadcast( (1 / 2) * (5 * c_s_rxav - 3 * c_s_surf_xav), [self.domain.lower() + " particle"], ) B = pybamm.PrimaryBroadcast((5 / 2) * (c_s_surf_xav - c_s_rxav), [self.domain.lower() + " particle"]) if self.domain == "Negative": # Since c_s_xav doesn't depend on x, we need to define a spatial # variable r which only has "negative particle" and "current # collector" as domains r = pybamm.SpatialVariable( "r_n", domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, coord_sys="spherical polar", ) c_s_xav = A + B * r**2 if self.domain == "Positive": # Since c_s_xav doesn't depend on x, we need to define a spatial # variable r which only has "positive particle" and "current # collector" as domains r = pybamm.SpatialVariable( "r_p", domain=["positive particle"], auxiliary_domains={"secondary": "current collector"}, coord_sys="spherical polar", ) c_s_xav = A + B * r**2 elif self.name == "quartic profile": # The concentration is given by c = A + B*r**2 + C*r**4 A = pybamm.PrimaryBroadcast( 39 * c_s_surf_xav / 4 - 3 * q_s_rxav - 35 * c_s_rxav / 4, [self.domain.lower() + " particle"], ) B = pybamm.PrimaryBroadcast( -35 * c_s_surf_xav + 10 * q_s_rxav + 35 * c_s_rxav, [self.domain.lower() + " particle"], ) C = pybamm.PrimaryBroadcast( 105 * c_s_surf_xav / 4 - 7 * q_s_rxav - 105 * c_s_rxav / 4, [self.domain.lower() + " particle"], ) if self.domain == "Negative": # Since c_s_xav doesn't depend on x, we need to define a spatial # variable r which only has "negative particle" and "current # collector" as domains r = pybamm.SpatialVariable( "r_n", domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, coord_sys="spherical polar", ) c_s_xav = A + B * r**2 + C * r**4 if self.domain == "Positive": # Since c_s_xav doesn't depend on x, we need to define a spatial # variable r which only has "positive particle" and "current # collector" as domains r = pybamm.SpatialVariable( "r_p", domain=["positive particle"], auxiliary_domains={"secondary": "current collector"}, coord_sys="spherical polar", ) c_s_xav = A + B * r**2 + C * r**4 c_s = pybamm.SecondaryBroadcast(c_s_xav, [self.domain.lower() + " electrode"]) c_s_surf = pybamm.PrimaryBroadcast( c_s_surf_xav, [self.domain.lower() + " electrode"]) # Set flux based on polynomial order if self.name == "uniform profile": # The flux is zero since there is no concentration gradient N_s_xav = pybamm.FullBroadcastToEdges( 0, self.domain.lower() + " particle", "current collector") elif self.name == "quadratic profile": # The flux may be computed directly from the polynomial for c if self.domain == "Negative": N_s_xav = (-self.param.D_n(c_s_xav, T_xav) * 5 * (c_s_surf_xav - c_s_rxav) * r) if self.domain == "Positive": N_s_xav = (-self.param.D_p(c_s_xav, T_xav) * 5 * (c_s_surf_xav - c_s_rxav) * r) elif self.name == "quartic profile": q_s_rxav = variables["Average " + self.domain.lower() + " particle concentration gradient"] # The flux may be computed directly from the polynomial for c if self.domain == "Negative": N_s_xav = -self.param.D_n(c_s_xav, T_xav) * ( (-70 * c_s_surf_xav + 20 * q_s_rxav + 70 * c_s_rxav) * r + (105 * c_s_surf_xav - 28 * q_s_rxav - 105 * c_s_rxav) * r**3) elif self.domain == "Positive": N_s_xav = -self.param.D_p(c_s_xav, T_xav) * ( (-70 * c_s_surf_xav + 20 * q_s_rxav + 70 * c_s_rxav) * r + (105 * c_s_surf_xav - 28 * q_s_rxav - 105 * c_s_rxav) * r**3) N_s = pybamm.SecondaryBroadcast(N_s_xav, [self._domain.lower() + " electrode"]) variables = self._get_standard_concentration_variables( c_s, c_s_av=c_s_rxav, c_s_surf=c_s_surf) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) return variables