def test_broadcast_to_edges(self): a = pybamm.Symbol("a") # primary broad_a = pybamm.PrimaryBroadcastToEdges(a, ["negative electrode"]) self.assertEqual(broad_a.name, "broadcast to edges") self.assertEqual(broad_a.children[0].name, a.name) self.assertEqual(broad_a.domain, ["negative electrode"]) self.assertTrue(broad_a.evaluates_on_edges("primary")) self.assertFalse(broad_a.broadcasts_to_nodes) self.assertEqual(broad_a.reduce_one_dimension(), a) # secondary a = pybamm.Symbol( "a", domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.SecondaryBroadcastToEdges(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.evaluates_on_edges("primary")) self.assertFalse(broad_a.broadcasts_to_nodes) # full a = pybamm.Symbol("a") broad_a = pybamm.FullBroadcastToEdges(a, ["negative electrode"], "current collector") self.assertEqual(broad_a.domain, ["negative electrode"]) self.assertEqual(broad_a.auxiliary_domains["secondary"], ["current collector"]) self.assertTrue(broad_a.evaluates_on_edges("primary")) self.assertFalse(broad_a.broadcasts_to_nodes) self.assertEqual( broad_a.reduce_one_dimension().id, pybamm.PrimaryBroadcastToEdges(a, "current collector").id, ) broad_a = pybamm.FullBroadcastToEdges(a, ["negative electrode"], {}) self.assertEqual(broad_a.reduce_one_dimension(), a) broad_a = pybamm.FullBroadcastToEdges( a, "negative particle", { "secondary": "negative electrode", "tertiary": "current collector" }, ) self.assertEqual( broad_a.reduce_one_dimension().id, pybamm.FullBroadcastToEdges(a, "negative electrode", "current collector").id, )
def get_coupled_variables(self, variables): c_s = variables[self.domain + " particle concentration"] c_s_rav = variables["R-averaged " + self.domain.lower() + " particle concentration"] c_s_surf = variables[self.domain + " particle surface concentration"] T = pybamm.PrimaryBroadcast( variables[self.domain + " electrode temperature"], [self.domain.lower() + " particle"], ) # Set flux depending on polynomial order if self.name == "uniform profile": # The flux is zero since there is no concentration gradient N_s = pybamm.FullBroadcastToEdges( 0, [self.domain.lower() + " particle"], auxiliary_domains={ "secondary": self.domain.lower() + " electrode", "tertiary": "current collector", }, ) 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": r = pybamm.standard_spatial_vars.r_n N_s = -self.param.D_n(c_s, T) * 5 * (c_s_surf - c_s_rav) * r elif self.domain == "Positive": r = pybamm.standard_spatial_vars.r_p N_s = -self.param.D_p(c_s, T) * 5 * (c_s_surf - c_s_rav) * r N_s_xav = pybamm.x_average(N_s) elif self.name == "quartic profile": q_s_rav = variables["R-averaged " + self.domain.lower() + " particle concentration gradient"] # The flux may be computed directly from the polynomial for c if self.domain == "Negative": r = pybamm.standard_spatial_vars.r_n N_s = -self.param.D_n(c_s, T) * ( (-70 * c_s_surf + 20 * q_s_rav + 70 * c_s_rav) * r + (105 * c_s_surf - 28 * q_s_rav - 105 * c_s_rav) * r**3) elif self.domain == "Positive": r = pybamm.standard_spatial_vars.r_p N_s = -self.param.D_p(c_s, T) * ( (-70 * c_s_surf + 20 * q_s_rav + 70 * c_s_rav) * r + (105 * c_s_surf - 28 * q_s_rav - 105 * c_s_rav) * r**3) N_s_xav = pybamm.x_average(N_s) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) return variables
def test_broadcast_to_edges(self): a = pybamm.Symbol("a") broad_a = pybamm.PrimaryBroadcastToEdges(a, ["negative electrode"]) self.assertEqual(broad_a.name, "broadcast to edges") self.assertEqual(broad_a.children[0].name, a.name) self.assertEqual(broad_a.domain, ["negative electrode"]) self.assertTrue(broad_a.evaluates_on_edges()) a = pybamm.Symbol( "a", domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.SecondaryBroadcastToEdges(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.evaluates_on_edges()) a = pybamm.Symbol("a") broad_a = pybamm.FullBroadcastToEdges(a, ["negative electrode"], "current collector") self.assertEqual(broad_a.domain, ["negative electrode"]) self.assertEqual(broad_a.auxiliary_domains["secondary"], ["current collector"]) self.assertTrue(broad_a.evaluates_on_edges())
def _flux_law(self, T): """Zero heat flux since temperature is constant""" q = pybamm.FullBroadcastToEdges( pybamm.Scalar(0), ["negative electrode", "separator", "positive electrode"], "current collector", ) return q
def _flux_law(self, T): """Fast heat diffusion (temperature has no spatial dependence)""" q = pybamm.FullBroadcastToEdges( pybamm.Scalar(0), ["negative electrode", "separator", "positive electrode"], "current collector", ) return q
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 get_coupled_variables(self, variables): N_e = pybamm.FullBroadcastToEdges( 0, ["negative electrode", "separator", "positive electrode"], "current collector", ) variables.update(self._get_standard_flux_variables(N_e)) return variables
def get_fundamental_variables(self): c_e_n = pybamm.FullBroadcast(1, "negative electrode", "current collector") c_e_s = pybamm.FullBroadcast(1, "separator", "current collector") c_e_p = pybamm.FullBroadcast(1, "positive electrode", "current collector") variables = self._get_standard_concentration_variables(c_e_n, c_e_s, c_e_p) N_e = pybamm.FullBroadcastToEdges( 0, ["negative electrode", "separator", "positive electrode"], "current collector", ) variables.update(self._get_standard_flux_variables(N_e)) return variables
def test_broadcast(self): whole_cell = ["negative electrode", "separator", "positive electrode"] a = pybamm.InputParameter("a") var = pybamm.Variable("var") # create discretisation disc = get_discretisation_for_testing() mesh = disc.mesh combined_submesh = mesh.combine_submeshes(*whole_cell) # scalar broad = disc.process_symbol(pybamm.FullBroadcast(a, whole_cell, {})) np.testing.assert_array_equal( broad.evaluate(inputs={"a": 7}), 7 * np.ones_like(combined_submesh[0].nodes[:, np.newaxis]), ) self.assertEqual(broad.domain, whole_cell) broad_disc = disc.process_symbol(broad) self.assertIsInstance(broad_disc, pybamm.Multiplication) self.assertIsInstance(broad_disc.children[0], pybamm.InputParameter) self.assertIsInstance(broad_disc.children[1], pybamm.Vector) # process Broadcast variable disc.y_slices = {var.id: [slice(1)]} broad1 = pybamm.FullBroadcast(var, ["negative electrode"], None) broad1_disc = disc.process_symbol(broad1) self.assertIsInstance(broad1_disc, pybamm.Multiplication) self.assertIsInstance(broad1_disc.children[0], pybamm.StateVector) self.assertIsInstance(broad1_disc.children[1], pybamm.Vector) # broadcast to edges broad_to_edges = pybamm.FullBroadcastToEdges(a, ["negative electrode"], None) broad_to_edges_disc = disc.process_symbol(broad_to_edges) np.testing.assert_array_equal( broad_to_edges_disc.evaluate(inputs={"a": 7}), 7 * np.ones_like(mesh["negative electrode"][0].edges[:, np.newaxis]), )
def get_coupled_variables(self, variables): N_e = pybamm.FullBroadcastToEdges( 0, ["negative electrode", "separator", "positive electrode"], "current collector", ) variables.update(self._get_standard_flux_variables(N_e)) c_e_av = pybamm.standard_variables.c_e_av c_e = pybamm.concatenation( pybamm.PrimaryBroadcast(c_e_av, ["negative electrode"]), pybamm.PrimaryBroadcast(c_e_av, ["separator"]), pybamm.PrimaryBroadcast(c_e_av, ["positive electrode"]), ) eps = variables["Porosity"] variables.update(self._get_total_concentration_electrolyte(c_e, eps)) return variables
def grad(symbol): """convenience function for creating a :class:`Gradient` Parameters ---------- symbol : :class:`Symbol` the gradient will be performed on this sub-symbol Returns ------- :class:`Gradient` the gradient of ``symbol`` """ # Gradient of a broadcast is zero if isinstance(symbol, pybamm.PrimaryBroadcast): new_child = pybamm.PrimaryBroadcast(0, symbol.child.domain) return pybamm.PrimaryBroadcastToEdges(new_child, symbol.domain) elif isinstance(symbol, pybamm.FullBroadcast): return pybamm.FullBroadcastToEdges(0, symbol.domain, symbol.auxiliary_domains) else: return Gradient(symbol)
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