def test_not_constant(self): a = pybamm.NotConstant(pybamm.Scalar(1)) self.assertEqual(a.name, "not_constant") self.assertEqual(a.domain, []) self.assertEqual(a.evaluate(), 1) self.assertEqual(a.jac(pybamm.StateVector(slice(0, 1))).evaluate(), 0) self.assertFalse(a.is_constant()) self.assertFalse((2 * a).is_constant())
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 _get_interface_variables_for_first_order(self, variables): # This is a bit of a hack, but we need to wrap electrolyte concentration with # the NotConstant class # to differentiate it from the electrolyte concentration inside the # surface potential difference when taking j.diff(c_e) later on c_e_0 = pybamm.NotConstant( variables["Leading-order x-averaged electrolyte concentration"]) hacked_variables = { **variables, self.domain + " electrolyte concentration": pybamm.PrimaryBroadcast(c_e_0, self.domain_for_broadcast), } delta_phi = variables["Leading-order x-averaged " + self.domain.lower() + " electrode surface potential difference"] j0 = self._get_exchange_current_density(hacked_variables) ne = self._get_number_of_electrons_in_reaction() ocp = self._get_open_circuit_potential(hacked_variables)[0] if j0.domain in ["current collector", ["current collector"]]: T = variables["X-averaged cell temperature"] else: T = variables[self.domain + " electrode temperature"] return c_e_0, delta_phi, j0, ne, ocp, T
def test_process_not_constant(self): param = pybamm.ParameterValues({"a": 4}) a = pybamm.NotConstant(pybamm.Parameter("a")) self.assertIsInstance(param.process_symbol(a), pybamm.NotConstant) self.assertEqual(param.process_symbol(a).evaluate(), 4)
def _process_symbol(self, symbol): """ See :meth:`ParameterValues.process_symbol()`. """ if isinstance(symbol, pybamm.Parameter): value = self[symbol.name] if isinstance(value, numbers.Number): # Scalar inherits name (for updating parameters) and domain (for # Broadcast) return pybamm.Scalar(value, name=symbol.name, domain=symbol.domain) elif isinstance(value, pybamm.Symbol): new_value = self.process_symbol(value) new_value.domain = symbol.domain return new_value else: raise TypeError("Cannot process parameter '{}'".format(value)) elif isinstance(symbol, pybamm.FunctionParameter): new_children = [] for child in symbol.children: if symbol.diff_variable is not None and any( x.id == symbol.diff_variable.id for x in child.pre_order()): # Wrap with NotConstant to avoid simplification, # which would stop symbolic diff from working properly new_child = pybamm.NotConstant(child.new_copy()) new_children.append(self.process_symbol(new_child)) else: new_children.append(self.process_symbol(child)) function_name = self[symbol.name] # Create Function or Interpolant or Scalar object if isinstance(function_name, tuple): # If function_name is a tuple then it should be (name, data) and we need # to create an Interpolant name, data = function_name function = pybamm.Interpolant(data[:, 0], data[:, 1], *new_children, name=name) # Define event to catch extrapolation. In these events the sign is # important: it should be positive inside of the range and negative # outside of it self.parameter_events.append( pybamm.Event( "Interpolant {} lower bound".format(name), pybamm.min(new_children[0] - min(data[:, 0])), pybamm.EventType.INTERPOLANT_EXTRAPOLATION, )) self.parameter_events.append( pybamm.Event( "Interpolant {} upper bound".format(name), pybamm.min(max(data[:, 0]) - new_children[0]), pybamm.EventType.INTERPOLANT_EXTRAPOLATION, )) elif isinstance(function_name, numbers.Number): # If the "function" is provided is actually a scalar, return a Scalar # object instead of throwing an error. # Also use ones_like so that we get the right shapes function = pybamm.Scalar( function_name, name=symbol.name) * pybamm.ones_like(*new_children) elif (isinstance(function_name, pybamm.Symbol) and function_name.evaluates_to_number()): # If the "function" provided is a pybamm scalar-like, use ones_like to # get the right shape # This also catches input parameters function = function_name * pybamm.ones_like(*new_children) elif callable(function_name): # otherwise evaluate the function to create a new PyBaMM object function = function_name(*new_children) elif isinstance(function_name, pybamm.Interpolant): function = function_name else: raise TypeError( "Parameter provided for '{}' ".format(symbol.name) + "is of the wrong type (should either be scalar-like or callable)" ) # Differentiate if necessary if symbol.diff_variable is None: function_out = function else: # return differentiated function new_diff_variable = self.process_symbol(symbol.diff_variable) function_out = function.diff(new_diff_variable) # Convert possible float output to a pybamm scalar if isinstance(function_out, numbers.Number): return pybamm.Scalar(function_out) # Process again just to be sure return self.process_symbol(function_out) elif isinstance(symbol, pybamm.BinaryOperator): # process children new_left = self.process_symbol(symbol.left) new_right = self.process_symbol(symbol.right) # Special case for averages, which can appear as "integral of a broadcast" # divided by "integral of a broadcast" # this construction seems very specific but can appear often when averaging if (isinstance(symbol, pybamm.Division) # right is integral(Broadcast(1)) and (isinstance(new_right, pybamm.Integral) and isinstance(new_right.child, pybamm.Broadcast) and new_right.child.child.id == pybamm.Scalar(1).id) # left is integral and isinstance(new_left, pybamm.Integral)): # left is integral(Broadcast) if (isinstance(new_left.child, pybamm.Broadcast) and new_left.child.child.domain == []): integrand = new_left.child if integrand.auxiliary_domains == {}: return integrand.orphans[0] else: domain = integrand.auxiliary_domains["secondary"] if "tertiary" not in integrand.auxiliary_domains: return pybamm.PrimaryBroadcast( integrand.orphans[0], domain) else: auxiliary_domains = { "secondary": integrand.auxiliary_domains["tertiary"] } return pybamm.FullBroadcast( integrand.orphans[0], domain, auxiliary_domains) # left is "integral of concatenation of broadcasts" elif isinstance(new_left.child, pybamm.Concatenation) and all( isinstance(child, pybamm.Broadcast) for child in new_left.child.children): return self.process_symbol(pybamm.x_average( new_left.child)) # make new symbol, ensure domain remains the same new_symbol = symbol._binary_new_copy(new_left, new_right) new_symbol.domain = symbol.domain return new_symbol # Unary operators elif isinstance(symbol, pybamm.UnaryOperator): new_child = self.process_symbol(symbol.child) new_symbol = symbol._unary_new_copy(new_child) # ensure domain remains the same new_symbol.domain = symbol.domain return new_symbol # Functions elif isinstance(symbol, pybamm.Function): new_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._function_new_copy(new_children) # Concatenations elif isinstance(symbol, pybamm.Concatenation): new_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._concatenation_new_copy(new_children) else: # Backup option: return new copy of the object try: return symbol.new_copy() except NotImplementedError: raise NotImplementedError( "Cannot process parameters for symbol of type '{}'".format( type(symbol)))
def __init__(self): super().__init__() self.name = "Effective resistance in current collector model (2D)" self.param = pybamm.LithiumIonParameters() # Set default length scales self.length_scales = { "current collector y": self.param.L_z, "current collector z": self.param.L_z, } # Get necessary parameters param = self.param l_cn = param.l_cn l_cp = param.l_cp l_tab_p = param.l_tab_p A_tab_p = l_cp * l_tab_p sigma_cn_dbl_prime = param.sigma_cn_dbl_prime sigma_cp_dbl_prime = param.sigma_cp_dbl_prime delta = param.delta # Set model variables -- we solve a auxilliary problem in each current collector # then relate this to the potentials and resistances later f_n = pybamm.Variable("Unit solution in negative current collector", domain="current collector") f_p = pybamm.Variable("Unit solution in positive current collector", domain="current collector") # Governing equations -- we impose that the average of f_p is zero # by introducing a Lagrange multiplier c = pybamm.Variable("Lagrange multiplier") self.algebraic = { f_n: pybamm.laplacian(f_n) + pybamm.source(1, f_n), f_p: pybamm.laplacian(f_p) - pybamm.source(1, f_p) + c * pybamm.DefiniteIntegralVector(f_p, vector_type="column"), c: pybamm.yz_average(f_p) + pybamm.NotConstant(0) * c, } # Boundary conditons pos_tab_bc = l_cp / A_tab_p self.boundary_conditions = { f_n: { "negative tab": (0, "Dirichlet"), "positive tab": (0, "Neumann") }, f_p: { "negative tab": (0, "Neumann"), "positive tab": (pos_tab_bc, "Neumann"), }, } # "Initial conditions" provides initial guess for solver self.initial_conditions = { f_n: pybamm.Scalar(0), f_p: pybamm.Scalar(0), c: pybamm.Scalar(0), } # Define effective current collector resistance R_cc_n = delta * pybamm.yz_average(f_n) / (l_cn * sigma_cn_dbl_prime) R_cc_p = (delta * pybamm.BoundaryIntegral(f_p, "positive tab") / (l_cp * sigma_cp_dbl_prime)) R_cc = R_cc_n + R_cc_p R_scale = param.potential_scale / param.I_typ self.variables = { "Unit solution in negative current collector": f_n, "Unit solution in positive current collector": f_p, "Effective current collector resistance": R_cc, "Effective current collector resistance [Ohm]": R_cc * R_scale, "Effective negative current collector resistance": R_cc_n, "Effective negative current collector resistance [Ohm]": R_cc_n * R_scale, "Effective positive current collector resistance": R_cc_p, "Effective positive current collector resistance [Ohm]": R_cc_p * R_scale, } # Add spatial variables var = pybamm.standard_spatial_vars L_y = param.L_y L_z = param.L_z self.variables.update({ "y": var.y, "y [m]": var.y * L_y, "z": var.z, "z [m]": var.z * L_z }) pybamm.citations.register("Timms2020")