def check_for_time_derivatives(self): # Check that no variable time derivatives exist in the rhs equations for key, eq in self.rhs.items(): for node in eq.pre_order(): if isinstance(node, pybamm.VariableDot): raise pybamm.ModelError( "time derivative of variable found " "({}) in rhs equation {}".format(node, key) ) if isinstance(node, pybamm.StateVectorDot): raise pybamm.ModelError( "time derivative of state vector found " "({}) in rhs equation {}".format(node, key) ) # Check that no variable time derivatives exist in the algebraic equations for key, eq in self.algebraic.items(): for node in eq.pre_order(): if isinstance(node, pybamm.VariableDot): raise pybamm.ModelError( "time derivative of variable found ({}) in algebraic" "equation {}".format(node, key) ) if isinstance(node, pybamm.StateVectorDot): raise pybamm.ModelError( "time derivative of state vector found ({}) in algebraic" "equation {}".format(node, key) )
def check_algebraic_equations(self, post_discretisation): """ Check that the algebraic equations are well-posed. Before discretisation, each algebraic equation key must appear in the equation After discretisation, there must be at least one StateVector in each algebraic equation """ if not post_discretisation: # After the model has been defined, each algebraic equation key should # appear in that algebraic equation # this has been relaxed for concatenations for now for var, eqn in self.algebraic.items(): if not any(x.id == var.id for x in eqn.pre_order()) and not isinstance( var, pybamm.Concatenation): raise pybamm.ModelError( "each variable in the algebraic eqn keys must appear in the eqn" ) else: # variables in keys don't get discretised so they will no longer match # with the state vectors in the algebraic equations. Instead, we check # that each algebraic equation contains some StateVector for eqn in self.algebraic.values(): if not any( isinstance(x, pybamm.StateVector) for x in eqn.pre_order()): raise pybamm.ModelError( "each algebraic equation must contain at least one StateVector" )
def check_initial_conditions(self, model): """Check initial conditions are a numpy array""" # Individual for var, eqn in model.initial_conditions.items(): assert isinstance( eqn.evaluate(t=0, inputs="shape test"), np.ndarray ), pybamm.ModelError( """ initial_conditions must be numpy array after discretisation but they are {} for variable '{}'. """.format( type(eqn.evaluate(t=0, inputs="shape test")), var ) ) # Concatenated assert ( type( model.concatenated_initial_conditions.evaluate(t=0, inputs="shape test") ) is np.ndarray ), pybamm.ModelError( """ Concatenated initial_conditions must be numpy array after discretisation but they are {}. """.format( type(model.concatenated_initial_conditions) ) )
def check_ics_bcs(self): """ Check that the initial and boundary conditions are well-posed. """ # Initial conditions for var in self.rhs.keys(): if var not in self.initial_conditions.keys(): raise pybamm.ModelError( """no initial condition given for variable '{}'""".format( var)) # Boundary conditions for var, eqn in {**self.rhs, **self.algebraic}.items(): if eqn.has_symbol_of_classes( (pybamm.Gradient, pybamm.Divergence )) and not eqn.has_symbol_of_classes(pybamm.Integral): # I have relaxed this check for now so that the lumped temperature # equation doesn't raise errors (this has and average in it) # Variable must be in the boundary conditions if not any(var.id == x.id for symbol in self.boundary_conditions.keys() for x in symbol.pre_order()): raise pybamm.ModelError(""" no boundary condition given for variable '{}' with equation '{}'. """.format(var, eqn))
def check_algebraic_equations(self, post_discretisation): """ Check that the algebraic equations are well-posed. Before discretisation, each algebraic equation key must appear in the equation After discretisation, there must be at least one StateVector in each algebraic equation """ vars_in_bcs = set() unpacker = pybamm.SymbolUnpacker(pybamm.Variable) for side_eqn in self.boundary_conditions.values(): all_vars = unpacker.unpack_list_of_symbols( [eqn for eqn, _ in side_eqn.values()]) vars_in_bcs.update(all_vars.keys()) if not post_discretisation: # After the model has been defined, each algebraic equation key should # appear in that algebraic equation, or in the boundary conditions # this has been relaxed for concatenations for now for var, eqn in self.algebraic.items(): if not (any(x.id == var.id for x in eqn.pre_order()) or var.id in vars_in_bcs or isinstance(var, pybamm.Concatenation)): raise pybamm.ModelError( "each variable in the algebraic eqn keys must appear in the eqn" ) else: # variables in keys don't get discretised so they will no longer match # with the state vectors in the algebraic equations. Instead, we check # that each algebraic equation contains some StateVector for eqn in self.algebraic.values(): if not eqn.has_symbol_of_classes(pybamm.StateVector): raise pybamm.ModelError( "each algebraic equation must contain at least one StateVector" )
def check_initial_conditions_rhs(self, model): """Check initial conditions and rhs have the same shape""" y0 = model.concatenated_initial_conditions # Individual for var in model.rhs.keys(): assert ( model.rhs[var].shape == model.initial_conditions[var].shape ), pybamm.ModelError( "rhs and initial_conditions must have the same shape after " "discretisation but rhs.shape = " "{} and initial_conditions.shape = {} for variable '{}'.". format(model.rhs[var].shape, model.initial_conditions[var].shape, var)) # Concatenated assert (model.concatenated_rhs.shape[0] + model.concatenated_algebraic.shape[0] == y0.shape[0] ), pybamm.ModelError(""" Concatenation of (rhs, algebraic) and initial_conditions must have the same shape after discretisation but rhs.shape = {}, algebraic.shape = {}, and initial_conditions.shape = {}. """.format( model.concatenated_rhs.shape, model.concatenated_algebraic.shape, y0.shape, ))
def check_well_determined(self, post_discretisation): """ Check that the model is not under- or over-determined. """ # Equations (differential and algebraic) # Get all the variables from differential and algebraic equations vars_in_rhs_keys = set() vars_in_algebraic_keys = set() vars_in_eqns = set() # Get all variables ids from rhs and algebraic keys and equations # For equations we look through the whole expression tree. # "Variables" can be Concatenations so we also have to look in the whole # expression tree for var, eqn in self.rhs.items(): vars_in_rhs_keys.update([ x.id for x in var.pre_order() if isinstance(x, pybamm.Variable) ]) vars_in_eqns.update([ x.id for x in eqn.pre_order() if isinstance(x, pybamm.Variable) ]) for var, eqn in self.algebraic.items(): vars_in_algebraic_keys.update([ x.id for x in var.pre_order() if isinstance(x, pybamm.Variable) ]) vars_in_eqns.update([ x.id for x in eqn.pre_order() if isinstance(x, pybamm.Variable) ]) # If any keys are repeated between rhs and algebraic then the model is # overdetermined if not set(vars_in_rhs_keys).isdisjoint(vars_in_algebraic_keys): raise pybamm.ModelError("model is overdetermined (repeated keys)") # If any algebraic keys don't appear in the eqns then the model is # overdetermined (but rhs keys can be absent from the eqns, e.g. dcdt = -1 is # fine) # Skip this step after discretisation, as any variables in the equations will # have been discretised to slices but keys will still be variables extra_algebraic_keys = vars_in_algebraic_keys.difference(vars_in_eqns) if extra_algebraic_keys and not post_discretisation: raise pybamm.ModelError( "model is overdetermined (extra algebraic keys)") # If any variables in the equations don't appear in the keys then the model is # underdetermined vars_in_keys = vars_in_rhs_keys.union(vars_in_algebraic_keys) extra_variables = vars_in_eqns.difference(vars_in_keys) if extra_variables: raise pybamm.ModelError( "model is underdetermined (too many variables)")
def build_coupled_variables(self): # Note: pybamm will try to get the coupled variables for the submodels in the # order they are set by the user. If this fails for a particular submodel, # return to it later and try again. If setting coupled variables fails and # there are no more submodels to try, raise an error. submodels = list(self.submodels.keys()) count = 0 # For this part the FuzzyDict of variables is briefly converted back into a # normal dictionary for speed with KeyErrors self._variables = dict(self._variables) while len(submodels) > 0: count += 1 for submodel_name, submodel in self.submodels.items(): if submodel_name in submodels: pybamm.logger.debug( "Getting coupled variables for {} submodel ({})". format(submodel_name, self.name)) try: self.variables.update( submodel.get_coupled_variables(self.variables)) submodels.remove(submodel_name) except KeyError as key: if len(submodels) == 1 or count == 100: # no more submodels to try raise pybamm.ModelError( """Submodel "{}" requires the variable {}, but it cannot be found. Check the selected submodels provide all of the required variables.""".format(submodel_name, key)) else: # try setting coupled variables on next loop through pybamm.logger.debug( "Can't find {}, trying other submodels first". format(key)) # Convert variables back into FuzzyDict self._variables = pybamm.FuzzyDict(self._variables)
def __init__(self, name, child, side): # side can only be "negative tab" or "positive tab" if domain is # "current collector" if side in ["negative tab", "positive tab"]: if child.domain[0] != "current collector": raise pybamm.ModelError( """Can only take boundary value on the tabs in the domain 'current collector', but {} has domain {}""".format( child, child.domain[0] ) ) self.side = side # boundary value of a child takes the domain from auxiliary domain of the child if child.auxiliary_domains != {}: domain = child.auxiliary_domains["secondary"] # if child has no auxiliary domain, integral removes domain else: domain = [] # tertiary auxiliary domain shift down to secondary try: auxiliary_domains = {"secondary": child.auxiliary_domains["tertiary"]} except KeyError: auxiliary_domains = {} super().__init__( name, child, domain=domain, auxiliary_domains=auxiliary_domains )
def check_variables(self): # Create list of all Variable nodes that appear in the model's list of variables unpacker = pybamm.SymbolUnpacker(pybamm.Variable) all_vars = unpacker.unpack_list_of_symbols(self.variables.values()) var_ids_in_keys = set() model_and_external_variables = (list(self.rhs.keys()) + list(self.algebraic.keys()) + self.external_variables) for var in model_and_external_variables: if isinstance(var, pybamm.Variable): var_ids_in_keys.add(var.id) # Key can be a concatenation elif isinstance(var, pybamm.Concatenation): var_ids_in_keys.update([child.id for child in var.children]) for var_id, var in all_vars.items(): if var_id not in var_ids_in_keys: raise pybamm.ModelError(""" No key set for variable '{}'. Make sure it is included in either model.rhs, model.algebraic, or model.external_variables in an unmodified form (e.g. not Broadcasted) """.format(var))
def check_variables(self, model): """ Check variables in variable list against rhs Be lenient with size check if the variable in model.variables is broadcasted, or a concatenation, or an outer product (if broadcasted, variable is a multiplication with a vector of ones) """ for rhs_var in model.rhs.keys(): if rhs_var.name in model.variables.keys(): var = model.variables[rhs_var.name] different_shapes = not np.array_equal(model.rhs[rhs_var].shape, var.shape) not_concatenation = not isinstance(var, pybamm.Concatenation) not_outer = not isinstance(var, pybamm.Outer) not_mult_by_one_vec = not ( isinstance(var, pybamm.Multiplication) and isinstance(var.right, pybamm.Vector) and np.all(var.right.entries == 1)) if (different_shapes and not_concatenation and not_outer and not_mult_by_one_vec): raise pybamm.ModelError(""" variable and its eqn must have the same shape after discretisation but variable.shape = {} and rhs.shape = {} for variable '{}'. """.format(var.shape, model.rhs[rhs_var].shape, var))
def diff(self, variable): if variable.id == self.id: return pybamm.Scalar(1) elif variable.id == pybamm.t.id: raise pybamm.ModelError("cannot take second time derivative of a Variable") else: return pybamm.Scalar(0)
def check_variables(self, model): """ Check variables in variable list against rhs Be lenient with size check if the variable in model.variables is broadcasted, or a concatenation (if broadcasted, variable is a multiplication with a vector of ones) """ for rhs_var in model.rhs.keys(): if rhs_var.name in model.variables.keys(): var = model.variables[rhs_var.name] different_shapes = not np.array_equal(model.rhs[rhs_var].shape, var.shape) not_concatenation = not isinstance(var, pybamm.Concatenation) not_mult_by_one_vec = not (isinstance( var, (pybamm.Multiplication, pybamm.MatrixMultiplication)) and (pybamm.is_matrix_one(var.left) or pybamm.is_matrix_one(var.right))) if different_shapes and not_concatenation and not_mult_by_one_vec: raise pybamm.ModelError( "variable and its eqn must have the same shape after " "discretisation but variable.shape = " "{} and rhs.shape = {} for variable '{}'. ".format( var.shape, model.rhs[rhs_var].shape, var))
def check_variables(self): # Create list of all Variable nodes that appear in the model's list of variables all_vars = {} for eqn in self.variables.values(): # Add all variables in the equation to the list of variables all_vars.update({ x.id: x for x in eqn.pre_order() if isinstance(x, pybamm.Variable) }) var_ids_in_keys = set() model_and_external_variables = (list(self.rhs.keys()) + list(self.algebraic.keys()) + self.external_variables) for var in model_and_external_variables: if isinstance(var, pybamm.Variable): var_ids_in_keys.add(var.id) # Key can be a concatenation elif isinstance(var, pybamm.Concatenation): var_ids_in_keys.update([child.id for child in var.children]) for var_id, var in all_vars.items(): if var_id not in var_ids_in_keys: raise pybamm.ModelError(""" No key set for variable '{}'. Make sure it is included in either model.rhs, model.algebraic, or model.external_variables in an unmodified form (e.g. not Broadcasted) """.format(var))
def __init__(self, filename, units="[]", current_scale=pybamm.electrical_parameters.I_typ): self.parameters = {"Current [A]": current_scale} self.parameters_eval = {"Current [A]": current_scale} # Load data from csv if filename: pybamm_path = pybamm.root_dir() data = pd.read_csv( os.path.join(pybamm_path, "input", "drive_cycles", filename), comment="#", skip_blank_lines=True, ).to_dict("list") self.time = np.array(data["time [s]"]) self.units = units self.current = np.array(data["current " + units]) # If voltage data is present, load it into the class try: self.voltage = np.array(data["voltage [V]"]) except KeyError: self.voltage = None else: raise pybamm.ModelError("No input file provided for current")
def check_ics_bcs(self): """Check that the initial and boundary conditions are well-posed.""" # Initial conditions for var in self.rhs.keys(): if var not in self.initial_conditions.keys(): raise pybamm.ModelError( """no initial condition given for variable '{}'""".format( var))
def check_and_combine_dict(self, dict1, dict2): # check that the key ids are distinct ids1 = set(x.id for x in dict1.keys()) ids2 = set(x.id for x in dict2.keys()) if len(ids1.intersection(ids2)) != 0: raise pybamm.ModelError( "Submodel incompatible: duplicate variables") dict1.update(dict2)
def check_tab_conditions(self, symbol, bcs): """ Check any boundary conditions applied on "negative tab", "positive tab" and "no tab". For 1D current collector meshes, these conditions are converted into boundary conditions on "left" (tab at z=0) or "right" (tab at z=l_z) depending on the tab location stored in the mesh. For 2D current collector meshes, the boundary conditions can be applied on the tabs directly. Parameters ---------- symbol : :class:`pybamm.expression_tree.symbol.Symbol` The symbol on which the boundary conditions are applied. bcs : dict The dictionary of boundary conditions (a dict of {side: equation}). Returns ------- dict The dictionary of boundary conditions, with the keys changed to "left" and "right" where necessary. """ # Check symbol domain domain = symbol.domain[0] mesh = self.mesh[domain][0] if domain != "current collector": raise pybamm.ModelError( """Boundary conditions can only be applied on the tabs in the domain 'current collector', but {} has domain {}""".format( symbol, domain ) ) # Replace keys with "left" and "right" as appropriate for 1D meshes if isinstance(mesh, pybamm.SubMesh1D): # send boundary conditions applied on the tabs to "left" or "right" # depending on the tab location stored in the mesh for tab in ["negative tab", "positive tab"]: if any(tab in side for side in list(bcs.keys())): bcs[mesh.tabs[tab]] = bcs.pop(tab) # if there was a tab at either end, then the boundary conditions # have now been set on "left" and "right" as required by the spatial # method, so there is no need to further modify the bcs dict if all(side in list(bcs.keys()) for side in ["left", "right"]): pass # if both tabs are located at z=0 then the "right" boundary condition # (at z=1) is the condition for "no tab" elif "left" in list(bcs.keys()): bcs["right"] = bcs.pop("no tab") # else if both tabs are located at z=1, the "left" boundary condition # (at z=0) is the condition for "no tab" else: bcs["left"] = bcs.pop("no tab") return bcs
def check_initial_conditions(self, model): # Check initial conditions are a numpy array # Individual for var, eqn in model.initial_conditions.items(): ic_eval = eqn.evaluate(t=0, inputs="shape test") if not isinstance(ic_eval, np.ndarray): raise pybamm.ModelError( "initial conditions must be numpy array after discretisation but " "they are {} for variable '{}'.".format( type(ic_eval), var)) # Check that the initial condition is within the bounds # Skip this check if there are input parameters in the initial conditions bounds = var.bounds if not eqn.has_symbol_of_classes(pybamm.InputParameter) and not ( all(bounds[0] <= ic_eval) and all(ic_eval <= bounds[1])): raise pybamm.ModelError( "initial condition is outside of variable bounds " "{} for variable '{}'.".format(bounds, var)) # Check initial conditions and model equations have the same shape # Individual for var in model.rhs.keys(): if model.rhs[var].shape != model.initial_conditions[var].shape: raise pybamm.ModelError( "rhs and initial conditions must have the same shape after " "discretisation but rhs.shape = " "{} and initial_conditions.shape = {} for variable '{}'.". format(model.rhs[var].shape, model.initial_conditions[var].shape, var)) for var in model.algebraic.keys(): if model.algebraic[var].shape != model.initial_conditions[ var].shape: raise pybamm.ModelError( "algebraic and initial conditions must have the same shape after " "discretisation but algebraic.shape = " "{} and initial_conditions.shape = {} for variable '{}'.". format( model.algebraic[var].shape, model.initial_conditions[var].shape, var, ))
def _concatenate_in_order(self, var_eqn_dict, check_complete=False): """ Concatenate a dictionary of {variable: equation} using self.y_slices The keys/variables in `var_eqn_dict` must be the same as the ids in `self.y_slices`. The resultant concatenation is ordered according to the ordering of the slice values in `self.y_slices` Parameters ---------- var_eqn_dict : dict Equations ({variable: equation} dict) to dicretise Returns ------- var_eqn_dict : dict Discretised right-hand side equations """ # Unpack symbols in variables that are concatenations of variables unpacked_variables = [] slices = [] for symbol in var_eqn_dict.keys(): if isinstance(symbol, pybamm.Concatenation): unpacked_variables.extend([var for var in symbol.children]) # must append the slice for the whole concatenation, so that equations # get sorted correctly slices.append( slice( self.y_slices[symbol.children[0].id][0].start, self.y_slices[symbol.children[-1].id][0].stop, )) else: unpacked_variables.append(symbol) slices.append(self.y_slices[symbol.id][0]) if check_complete: # Check keys from the given var_eqn_dict against self.y_slices ids = {v.id for v in unpacked_variables} if ids != self.y_slices.keys(): given_variable_names = [v.name for v in var_eqn_dict.keys()] raise pybamm.ModelError( "Initial conditions are insufficient. Only " "provided for {} ".format(given_variable_names)) equations = list(var_eqn_dict.values()) # sort equations according to slices sorted_equations = [eq for _, eq in sorted(zip(slices, equations))] return self.concatenate(*sorted_equations)
def solve(self, model, t_eval): """ Execute the solver setup and calculate the solution of the model at specified times. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions t_eval : numeric type The times at which to compute the solution Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ pybamm.logger.info("Start solving {}".format(model.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: raise pybamm.ModelError("Cannot solve empty model") # Set up timer = pybamm.Timer() start_time = timer.time() if model.convert_to_format == "casadi" or isinstance(self, pybamm.CasadiSolver): self.set_up_casadi(model) else: self.set_up(model) set_up_time = timer.time() - start_time # Solve solution, solve_time, termination = self.compute_solution(model, t_eval) # Assign times solution.solve_time = solve_time solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) pybamm.logger.info( "Set-up time: {}, Solve time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), timer.format(solution.total_time), ) ) return solution
def boundary_conditions(self, boundary_conditions): # Convert any numbers to a pybamm.Scalar for var, bcs in boundary_conditions.items(): for side, bc in bcs.items(): if isinstance(bc[0], numbers.Number): # typ is the type of the bc, e.g. "Dirichlet" or "Neumann" eqn, typ = boundary_conditions[var][side] boundary_conditions[var][side] = (pybamm.Scalar(eqn), typ) # Check types if bc[1] not in ["Dirichlet", "Neumann"]: raise pybamm.ModelError(""" boundary condition types must be Dirichlet or Neumann, not '{}' """.format(bc[1])) self._boundary_conditions = boundary_conditions
def check_no_repeated_keys(self): "Check that no equation keys are repeated" rhs_alg = {**self.rhs, **self.algebraic} rhs_alg_keys = [] for var in rhs_alg.keys(): # Check the variable has not already been defined if var.id in rhs_alg_keys: raise pybamm.ModelError( "Multiple equations specified for variable {!r}".format( var)) # Update list of variables else: rhs_alg_keys.append(var.id)
def interpolate(self): " Creates the interpolant from the loaded data " # If data is dimenionless, multiply by a typical current (e.g. data # could be C-rate and current_scale the 1C discharge current). Otherwise, # just import the current data. if self.units == "[]": current = self.parameters_eval["Current [A]"] * self.current elif self.units == "[A]": current = self.current else: raise pybamm.ModelError( "Current data must have units [A] or be dimensionless") # Interpolate using Piecewise Cubic Hermite Interpolating Polynomial # (does not overshoot non-smooth data) self.current_interp = interp.PchipInterpolator(self.time, current)
def check_algebraic_equations(self, post_discretisation): """ Check that the algebraic equations are well-posed. After discretisation, there must be at least one StateVector in each algebraic equation. """ if post_discretisation: # Check that each algebraic equation contains some StateVector for eqn in self.algebraic.values(): if not eqn.has_symbol_of_classes(pybamm.StateVector): raise pybamm.ModelError( "each algebraic equation must contain at least one StateVector" ) else: # We do not perfom any checks before discretisation (most problematic # cases should be caught by `check_well_determined`) pass
def process_boundary_conditions(self, model): """Discretise model boundary_conditions, also converting keys to ids Parameters ---------- model : :class:`pybamm.BaseModel` Model to dicretise. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation}) Returns ------- dict Dictionary of processed boundary conditions """ processed_bcs = {} # process and set pybamm.variables first incase required # in discrisation of other boundary conditions for key, bcs in model.boundary_conditions.items(): processed_bcs[key.id] = {} # check if the boundary condition at the origin for sphere domains is other # than no flux if key not in model.external_variables: for subdomain in key.domain: if self.mesh[subdomain].coord_sys == "spherical polar": if bcs["left"][0].value != 0 or bcs["left"][ 1] != "Neumann": raise pybamm.ModelError( "Boundary condition at r = 0 must be a homogeneous " "Neumann condition for {} coordinates".format( self.mesh[subdomain].coord_sys)) # Handle any boundary conditions applied on the tabs if any("tab" in side for side in list(bcs.keys())): bcs = self.check_tab_conditions(key, bcs) # Process boundary conditions for side, bc in bcs.items(): eqn, typ = bc pybamm.logger.debug("Discretise {} ({} bc)".format(key, side)) processed_eqn = self.process_symbol(eqn) processed_bcs[key.id][side] = (processed_eqn, typ) return processed_bcs
def check_tab_conditions(self, symbol, bcs): """ Check any boundary conditions applied on "negative tab", "positive tab" and "no tab". For 1D current collector meshes, these conditions are converted into boundary conditions on "left" (tab at z=0) or "right" (tab at z=l_z) depending on the tab location stored in the mesh. For 2D current collector meshes, the boundary conditions can be applied on the tabs directly. Parameters ---------- symbol : :class:`pybamm.expression_tree.symbol.Symbol` The symbol on which the boundary conditions are applied. bcs : dict The dictionary of boundary conditions (a dict of {side: equation}). Returns ------- dict The dictionary of boundary conditions, with the keys changed to "left" and "right" where necessary. """ # Check symbol domain domain = symbol.domain[0] mesh = self.mesh[domain][0] if domain != "current collector": raise pybamm.ModelError( """Boundary conditions can only be applied on the tabs in the domain 'current collector', but {} has domain {}""".format( symbol, domain)) # Replace keys with "left" and "right" as appropriate for 1D meshes if isinstance(mesh, pybamm.SubMesh1D): # replace negative and/or positive tab for tab in ["negative tab", "positive tab"]: if any(tab in side for side in list(bcs.keys())): bcs[mesh.tabs[tab]] = bcs.pop(tab) # replace no tab if any("no tab" in side for side in list(bcs.keys())): if "left" in list(bcs.keys()): bcs["right"] = bcs.pop("no tab") # tab at bottom else: bcs["left"] = bcs.pop("no tab") # tab at top return bcs
def stiffness_matrix(self, symbol, boundary_conditions): """ Laplacian (stiffness) matrix for finite elements in the appropriate domain. Parameters ---------- symbol: :class:`pybamm.Symbol` The symbol for which we want to calculate the laplacian matrix boundary_conditions : dict The boundary conditions of the model ({symbol.id: {"negative tab": neg. tab bc, "positive tab": pos. tab bc}}) Returns ------- :class:`pybamm.Matrix` The (sparse) finite element stiffness matrix for the domain """ # get primary domain mesh domain = symbol.domain[0] mesh = self.mesh[domain][0] # make form for the stiffness @skfem.bilinear_form def stiffness_form(u, du, v, dv, w): return sum(du * dv) # assemble the stifnness matrix stiffness = skfem.asm(stiffness_form, mesh.basis) # get boundary conditions and type try: _, neg_bc_type = boundary_conditions[symbol.id]["negative tab"] _, pos_bc_type = boundary_conditions[symbol.id]["positive tab"] except KeyError: raise pybamm.ModelError( "No boundary conditions provided for symbol `{}``".format( symbol)) # adjust matrix for Dirichlet boundary conditions if neg_bc_type == "Dirichlet": self.bc_apply(stiffness, mesh.negative_tab_dofs) if pos_bc_type == "Dirichlet": self.bc_apply(stiffness, mesh.positive_tab_dofs) return pybamm.Matrix(stiffness)
def build_model(self): # Check if already built if self._built: raise pybamm.ModelError( """Model already built. If you are adding a new submodel, try using `model.update` instead.""") pybamm.logger.info("Start building {}".format(self.name)) if self._built_fundamental_and_external is False: self.build_fundamental_and_external() self.build_coupled_variables() self.build_model_equations() pybamm.logger.debug("Setting voltage variables ({})".format(self.name)) self.set_voltage_variables() pybamm.logger.debug("Setting SoC variables ({})".format(self.name)) self.set_soc_variables() # Massive hack for consistent delta_phi = phi_s - phi_e with SPMe # This needs to be corrected if isinstance(self, pybamm.lithium_ion.SPMe): for domain in ["Negative", "Positive"]: phi_s = self.variables[domain + " electrode potential"] phi_e = self.variables[domain + " electrolyte potential"] delta_phi = phi_s - phi_e s = self.submodels[domain.lower() + " interface"] var = s._get_standard_surface_potential_difference_variables( delta_phi) self.variables.update(var) self._built = True pybamm.logger.info("Finish building {}".format(self.name))
def process_model(self, unprocessed_model, inplace=True): """Assign parameter values to a model. Currently inplace, could be changed to return a new model. Parameters ---------- unprocessed_model : :class:`pybamm.BaseModel` Model to assign parameter values for inplace: bool, optional If True, replace the parameters in the model in place. Otherwise, return a new model with parameter values set. Default is True. Raises ------ :class:`pybamm.ModelError` If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and `model.variables = {}`) """ pybamm.logger.info("Start setting parameters for {}".format( unprocessed_model.name)) # set up inplace vs not inplace if inplace: # any changes to unprocessed_model attributes will change model attributes # since they point to the same object model = unprocessed_model else: # create a blank model of the same class model = unprocessed_model.new_empty_copy() if (len(unprocessed_model.rhs) == 0 and len(unprocessed_model.algebraic) == 0 and len(unprocessed_model.variables) == 0): raise pybamm.ModelError( "Cannot process parameters for empty model") new_rhs = {} for variable, equation in unprocessed_model.rhs.items(): pybamm.logger.verbose( "Processing parameters for {!r} (rhs)".format(variable)) new_rhs[variable] = self.process_symbol(equation) model.rhs = new_rhs new_algebraic = {} for variable, equation in unprocessed_model.algebraic.items(): pybamm.logger.verbose( "Processing parameters for {!r} (algebraic)".format(variable)) new_algebraic[variable] = self.process_symbol(equation) model.algebraic = new_algebraic new_initial_conditions = {} for variable, equation in unprocessed_model.initial_conditions.items(): pybamm.logger.verbose( "Processing parameters for {!r} (initial conditions)".format( variable)) new_initial_conditions[variable] = self.process_symbol(equation) model.initial_conditions = new_initial_conditions model.boundary_conditions = self.process_boundary_conditions( unprocessed_model) new_variables = {} for variable, equation in unprocessed_model.variables.items(): pybamm.logger.verbose( "Processing parameters for {!r} (variables)".format(variable)) new_variables[variable] = self.process_symbol(equation) model.variables = new_variables new_events = [] for event in unprocessed_model.events: pybamm.logger.verbose( "Processing parameters for event '{}''".format(event.name)) new_events.append( pybamm.Event(event.name, self.process_symbol(event.expression), event.event_type)) for event in self.parameter_events: pybamm.logger.verbose( "Processing parameters for event '{}''".format(event.name)) new_events.append( pybamm.Event(event.name, self.process_symbol(event.expression), event.event_type)) model.events = new_events # Set external variables model.external_variables = [ self.process_symbol(var) for var in unprocessed_model.external_variables ] # Process timescale model.timescale = self.process_symbol(unprocessed_model.timescale) # Process length scales new_length_scales = {} for domain, scale in unprocessed_model.length_scales.items(): new_length_scales[domain] = self.process_symbol(scale) model.length_scales = new_length_scales pybamm.logger.info("Finish setting parameters for {}".format( model.name)) return model