Source code for pybamm.meshes.scikit_fem_submeshes

#
# scikit-fem meshes for use in PyBaMM
#
import pybamm
from .meshes import SubMesh
import numpy as np

from pybamm.util import import_optional_dependency


[docs] class ScikitSubMesh2D(SubMesh): """ 2D submesh class. Contains information about the 2D finite element mesh. Note: This class only allows for the use of piecewise-linear triangular finite elements. Parameters ---------- edges : array_like An array containing the points corresponding to the edges of the submesh coord_sys : string The coordinate system of the submesh tabs : dict, optional A dictionary that contains information about the size and location of the tabs """ def __init__(self, edges, coord_sys, tabs): skfem = import_optional_dependency("skfem") self.edges = edges self.nodes = dict.fromkeys(["y", "z"]) for var in self.nodes.keys(): self.nodes[var] = (self.edges[var][1:] + self.edges[var][:-1]) / 2 self.npts = len(self.edges["y"]) * len(self.edges["z"]) self.coord_sys = coord_sys # save tabs for serialisation self.tabs = tabs # create mesh self.fem_mesh = skfem.MeshTri.init_tensor(self.edges["y"], self.edges["z"]) # get coordinates (returns a vector size 2*(Ny*Nz)) self.coordinates = self.fem_mesh.p # create elements and basis self.element = skfem.ElementTriP1() self.basis = skfem.InteriorBasis(self.fem_mesh, self.element) self.facet_basis = skfem.FacetBasis(self.fem_mesh, self.element) # get degrees of freedom and facets which correspond to tabs, and # create facet basis for sub regions self.negative_tab_dofs = self.basis.get_dofs( lambda x: self.on_boundary(x[0], x[1], tabs["negative"]) ).all() self.positive_tab_dofs = self.basis.get_dofs( lambda x: self.on_boundary(x[0], x[1], tabs["positive"]) ).all() self.negative_tab_facets = self.fem_mesh.facets_satisfying( lambda x: self.on_boundary(x[0], x[1], tabs["negative"]) ) self.positive_tab_facets = self.fem_mesh.facets_satisfying( lambda x: self.on_boundary(x[0], x[1], tabs["positive"]) ) self.negative_tab_basis = skfem.FacetBasis( self.fem_mesh, self.element, facets=self.negative_tab_facets ) self.positive_tab_basis = skfem.FacetBasis( self.fem_mesh, self.element, facets=self.positive_tab_facets ) def read_lims(self, lims): # Separate limits and tabs # Read and remove tabs. If "tabs" is not a key in "lims", then tabs is set to # "None" and nothing is removed from lims tabs = lims.pop("tabs", None) # check that two variables have been passed in if len(lims) != 2: raise pybamm.GeometryError( f"lims should contain exactly two variables, not {len(lims)}" ) # get spatial variables spatial_vars = list(lims.keys()) for i, var in enumerate(spatial_vars): if isinstance(var, str): spatial_vars[i] = getattr(pybamm.standard_spatial_vars, var) # check coordinate system agrees if spatial_vars[0].coord_sys != spatial_vars[1].coord_sys: raise pybamm.DomainError( f"""spatial variables should have the same coordinate system, but have coordinate systems {spatial_vars[0].coord_sys} and {spatial_vars[1].coord_sys}""" ) return spatial_vars, tabs
[docs] def on_boundary(self, y, z, tab): """ A method to get the degrees of freedom corresponding to the subdomains for the tabs. """ l_y = self.edges["y"][-1] l_z = self.edges["z"][-1] def near(x, point, tol=3e-16): return abs(x - point) < tol def between(x, interval, tol=3e-16): return x > interval[0] - tol and x < interval[1] + tol # Tab on top if near(tab["z_centre"], l_z): tab_left = tab["y_centre"] - tab["width"] / 2 tab_right = tab["y_centre"] + tab["width"] / 2 return [ near(Z, l_z) and between(Y, [tab_left, tab_right]) for Y, Z in zip(y, z) ] # Tab on bottom elif near(tab["z_centre"], 0): tab_left = tab["y_centre"] - tab["width"] / 2 tab_right = tab["y_centre"] + tab["width"] / 2 return [ near(Z, 0) and between(Y, [tab_left, tab_right]) for Y, Z in zip(y, z) ] # Tab on left elif near(tab["y_centre"], 0): tab_bottom = tab["z_centre"] - tab["width"] / 2 tab_top = tab["z_centre"] + tab["width"] / 2 return [ near(Y, 0) and between(Z, [tab_bottom, tab_top]) for Y, Z in zip(y, z) ] # Tab on right elif near(tab["y_centre"], l_y): tab_bottom = tab["z_centre"] - tab["width"] / 2 tab_top = tab["z_centre"] + tab["width"] / 2 return [ near(Y, l_y) and between(Z, [tab_bottom, tab_top]) for Y, Z in zip(y, z) ] else: raise pybamm.GeometryError("tab location not valid")
def to_json(self): json_dict = { "edges": {k: v.tolist() for k, v in self.edges.items()}, "coord_sys": self.coord_sys, "tabs": self.tabs, } return json_dict
[docs] class ScikitUniform2DSubMesh(ScikitSubMesh2D): """ Contains information about the 2D finite element mesh with uniform grid spacing (can be different spacing in y and z). Note: This class only allows for the use of piecewise-linear triangular finite elements. Parameters ---------- lims : dict A dictionary that contains the limits of each spatial variable npts : dict A dictionary that contains the number of points to be used on each spatial variable """ def __init__(self, lims, npts): spatial_vars, tabs = self.read_lims(lims) coord_sys = spatial_vars[0].coord_sys # compute edges edges = {} for var in spatial_vars: if var.name not in ["y", "z"]: raise pybamm.DomainError( f"spatial variable must be y or z not {var.name}" ) else: edges[var.name] = np.linspace( lims[var.name]["min"], lims[var.name]["max"], npts[var.name] ) super().__init__(edges, coord_sys, tabs) @classmethod def _from_json(cls, snippet: dict): instance = cls.__new__(cls) edges = {k: np.array(v) for k, v in snippet["edges"].items()} super(ScikitUniform2DSubMesh, instance).__init__( edges, snippet["coord_sys"], snippet["tabs"] ) return instance
[docs] class ScikitExponential2DSubMesh(ScikitSubMesh2D): """ Contains information about the 2D finite element mesh generated by taking the tensor product of a uniformly spaced grid in the y direction, and a unequally spaced grid in the z direction in which the points are clustered close to the top boundary using an exponential formula on the interval [a,b]. The gridpoints in the z direction are given by .. math:: z_{k} = (b-a) + \\frac{\\exp{-\\alpha k / N} - 1}{\\exp{-\\alpha} - 1} + a, for k = 1, ..., N, where N is the number of nodes. Here alpha is a stretching factor. As the number of gridpoints tends to infinity, the ratio of the largest and smallest grid cells tends to exp(alpha). Note: in the future this will be extended to allow points to be clustered near any of the boundaries. Parameters ---------- lims : dict A dictionary that contains the limits of each spatial variable npts : dict A dictionary that contains the number of points to be used on each spatial variable side : str, optional Whether the points are clustered near to a particular boundary. At present, can only be "top". Default is "top". stretch : float, optional The factor (alpha) which appears in the exponential. Default is 2.3. """ def __init__(self, lims, npts, side="top", stretch=2.3): # check side is top if side != "top": raise pybamm.GeometryError( f"At present, side can only be 'top', but is set to {side}" ) spatial_vars, tabs = self.read_lims(lims) coord_sys = spatial_vars[0].coord_sys # compute edges edges = {} for var in spatial_vars: if var.name not in ["y", "z"]: raise pybamm.DomainError( f"spatial variable must be y or z not {var.name}" ) elif var.name == "y": edges[var.name] = np.linspace( lims[var.name]["min"], lims[var.name]["max"], npts[var.name] ) elif var.name == "z": ii = np.array(range(0, npts[var.name])) a = lims[var.name]["min"] b = lims[var.name]["max"] edges[var.name] = (b - a) * ( np.exp(-stretch * ii / (npts[var.name] - 1)) - 1 ) / (np.exp(-stretch) - 1) + a super().__init__(edges, coord_sys, tabs)
[docs] class ScikitChebyshev2DSubMesh(ScikitSubMesh2D): """ Contains information about the 2D finite element mesh generated by taking the tensor product of two 1D meshes which use Chebyshev nodes on the interval (a, b), given by .. math:: x_{k} = \\frac{1}{2}(a+b) + \\frac{1}{2}(b-a) \\cos(\\frac{2k-1}{2N}\\pi), for k = 1, ..., N, where N is the number of nodes. Note: this mesh then appends the boundary edgess, so that the 1D mesh edges are given by .. math :: a < x_{1} < ... < x_{N} < b. Note: This class only allows for the use of piecewise-linear triangular finite elements. Parameters ---------- lims : dict A dictionary that contains the limits of each spatial variable npts : dict A dictionary that contains the number of points to be used on each spatial variable """ def __init__(self, lims, npts): spatial_vars, tabs = self.read_lims(lims) coord_sys = spatial_vars[0].coord_sys # compute edges edges = {} for var in spatial_vars: if var.name not in ["y", "z"]: raise pybamm.DomainError( f"spatial variable must be y or z not {var.name}" ) else: # Create N Chebyshev nodes in the interval (a,b) N = npts[var.name] - 2 ii = np.array(range(1, N + 1)) a = lims[var.name]["min"] b = lims[var.name]["max"] x_cheb = (a + b) / 2 + (b - a) / 2 * np.cos( (2 * ii - 1) * np.pi / 2 / N ) # Append the boundary nodes. Note: we need to flip the order the # Chebyshev nodes as they are created in descending order. edges[var.name] = np.concatenate(([a], np.flip(x_cheb), [b])) super().__init__(edges, coord_sys, tabs)
[docs] class UserSupplied2DSubMesh(ScikitSubMesh2D): """ A class to generate a tensor product submesh on a 2D domain by using two user supplied vectors of edges: one for the y-direction and one for the z-direction. Note: this mesh should be created using :class:`UserSupplied2DSubMeshGenerator`. Parameters ---------- lims : dict A dictionary that contains the limits of the spatial variables npts : dict A dictionary that contains the number of points to be used on each spatial variable. Note: the number of nodes (located at the cell centres) is npts, and the number of edges is npts+1. y_edges : array_like The array of points which correspond to the edges in the y direction of the mesh. z_edges : array_like The array of points which correspond to the edges in the z direction of the mesh. """ def __init__(self, lims, npts, y_edges=None, z_edges=None): # raise error if no edges passed if y_edges is None: raise pybamm.GeometryError("User mesh requires parameter 'y_edges'") if z_edges is None: raise pybamm.GeometryError("User mesh requires parameter 'z_edges'") spatial_vars, tabs = self.read_lims(lims) coord_sys = spatial_vars[0].coord_sys # check and store edges edges = {"y": y_edges, "z": z_edges} for var in spatial_vars: # check that npts equals number of user-supplied edges if npts[var.name] != len(edges[var.name]): raise pybamm.GeometryError( f"""User-suppled edges has should have length npts but has length {len(edges[var.name])}. Number of points (npts) for variable {var.name} in domain {var.domain} is {npts[var.name]}.""" ) # check end points of edges agree with spatial_lims if edges[var.name][0] != lims[var.name]["min"]: raise pybamm.GeometryError( """First entry of edges is {}, but should be equal to {} for variable {} in domain {}.""".format( edges[var.name][0], lims[var.name]["min"], var.name, var.domain ) ) if edges[var.name][-1] != lims[var.name]["max"]: raise pybamm.GeometryError( """Last entry of edges is {}, but should be equal to {} for variable {} in domain {}.""".format( edges[var.name][-1], lims[var.name]["max"], var.name, var.domain ) ) super().__init__(edges, coord_sys=coord_sys, tabs=tabs)