Chapter 10
Data structures & implementation

Context: In this chapter, we will show how a simple finite element solver can be implemented in the Python programming language using our example of solving the Poisson equation. Here, we assume that the charge density vanishes. Thus, we calculate the spatial distribution of the electrostatic potential under appropriate boundary conditions. We will use this to calculate the capacitance of a plate capacitor.

10.1 Example problem

We will now further develop our example problem from the previous chapter and calculate the capacitance of a plate capacitor. To do this, we assume an infinitesimal charge density in the capacitor, \(\rho =0\). The Poisson equation then becomes the Laplace equation, \begin {equation} \nabla ^2 \Phi = 0. \end {equation} The plates of the capacitor are assumed to be metallic, i.e. the potential on the capacitor plates is constant (see Fig. 10.1a). This is modeled by a Dirichlet boundary condition. The rest of the domain is given a Neumann boundary condition, in which the derivative on the surface disappears.

PIC

Figure 10.1: (a) Geometry of the plate capacitor considered in this chapter. The potential \(\Phi \) is constant on the electrodes. (b) Section of the boundary with integration area for deriving the interpretation of the directional derivative at the boundary.

In the context of the Poisson or Laplace equation, directional derivatives at the boundary have a simple interpretation. We look at a small volume element \(\Delta \Omega \) at the boundary of the area (see Fig. 10.1b). Integrating the Poisson equation over this area yields, \begin {equation} \int _{\Delta \Omega }\dif ^3 r\, \nabla ^2 \Phi = \int _{\partial \Delta \Omega }\dif ^2 r\, \nabla \Phi \cdot \hat {n}(\v {r}) = \frac {1}{\varepsilon } \int _{\Delta \Omega }\dif ^3 r\, \rho (\v {r}), \end {equation} where the integration over \(\partial \Delta \Omega \) is done along the path shown in Fig. Now, we assume that the two sides of the path that are perpendicular to the boundary of the domain are negligible compared to the other two sides. Furthermore, we assume that only a surface charge \(\sigma (\v {r})\) lives in the domain. This yields \begin {equation} \int _{\partial \Delta \Omega }\dif ^2 r \ \hat {n}(\v {r}) \cdot \nabla \Phi = \frac {1}{\varepsilon } \int _{\Delta A} \dif ^2 r \ \sigma (\v {r}), \end {equation} where \(\Delta A\) is the area of the boundary of the simulation domain that lies within \(\Delta \Omega \). Assuming that the space outside the simulation domain is field-free, i.e. \(\nabla \Phi =0\), then \begin {equation} \nabla \Phi \cdot \hat {n}(\v {r}) = \frac {\sigma (\v {r})}{\varepsilon }, \label {eq:surfacecharge} \end {equation} the directional derivative thus yields the surface charge at the boundary.

The absence of a field outside our simulation domain is only exactly fulfilled at the electrodes. These are metallic and therefore by definition free of fields. (A field inside an ideal metal immediately leads to a rearrangement of charges that then compensate for this field). This means that we can use Eq. \eqref{eq:surfacecharge} to calculate the charge induced on the capacitor plates. Together with the potential given by the Dirichlet boundary conditions, this can be used to calculate the capacitance on the capacitor plates.

On the other hand, the implicit Neumann boundary condition \(\nabla \Phi \cdot \hat {n}=0\) means that our simulation is carried out under conditions in which the boundary is charge-free but outside the simulation domain the field disappears. This is an artificial condition that can cause errors. So you have to make sure that the simulation domain is large enough in the direction parallel to the capacitor plates to properly capture the stray fields at the edge of the capacitor.

10.2 Data Structures

The central data structure for numerical applications is the multidimensional array, numpy.ndarray, of the numpy library. Multidimensional arrays reserve memory for a certain number of entries. (These entries are also called elements, but in order to avoid confusion with finite elements, they will be referred to throughout this document as entries.) An array of length \(10\) filled with zeros (i.e. \(10\) entries) is obtained by

1import numpy as np
2a = np.zeros(10)

This array has the dimension \(1\). The multidimensional character of the arrays manifests itself in the fact that the arrays implicitly implement a mapping of several coordinates to a linear index. A two-dimensional array can be obtained, for example, by

1b = np.zeros([2, 5])

You can now access the array entries, e.g.

1a[6]
2b[1, 1]

Both commands access entry \(6\) of the underlying memory area. A natural use of multidimensional arrays is the representation of vectors (\(1\)-dimensional arrays) or matrices (\(2\)-dimensional arrays).

However, we will use these properties in the optimized, vectorized implementation of the example problem in these chapters to map the structure of our grid.

Note: The statement that

1a[6]
2b[1, 1]

accesses the same entry of the underlying memory area in the above example depends on the storage order. It is only true if the last index is stored compactly in memory. This storage order is called “row major” because in a two-dimensional array, i.e. a matrix, the rows are stored compactly in memory. If the first index is the compact one, it is called “column major”. In numpy, “row major” is also called “C-continguous” and “column major” is called “F-contiguous”. This is because in the C programming language the memory order is “row major” and in the Fortran programming language the memory order is “column major”. Arrays in numpy are “column major” by default, but other memory orders are supported and do occur.

10.3 Initialization

The example implementations follow simple rules for readable computer code. This should always be written in a way that a third person can read and reuse it. We will therefore...

  1. ...use only English language.
  2. ...write out variable names and do not use symbols as variable names (e.g. potential and not the written out symbol phi as the name).
  3. ...add a suffix to array variables that indicates the type of indices (e.g. potential_xy to indicate that there are two indices corresponding to the positions \(x\) and \(y\)).
  4. ...document the code with comment blocks and Python docstrings. We recommend the numpydoc standard for docstrings.

In this implementation, we use explicit loops to improve the readability of the code. The code can still be vectorized by using numpy operations.

First, we have to initialize the code and determine how many grid points we want to use. We define the variables

1# Grid size, number of nodes
2nb_nodes = 32, 32
3Nx, Ny = nb_nodes

We now also determine the range over which the two electrodes of the capacitor should extend:

1# Top capacitor plate
2top_left = Nx//4
3top_right = 3*Nx//4-1
4top_potential = 1
5
6# Bottom capacitor plate
7bottom_left = Nx//4
8bottom_right = 3*Nx//4-1
9bottom_potential = -1

The area is specified here with node indices. Furthermore, we still need the element matrix that we store in a numpy.ndarray:

1# Element matrix, index l indicates element-local node
2element_matrix_ll = np.array([[1, -1/2, -1/2],
3[-1/2, 1/2, 0],
4[-1/2, 0, 1/2]])

The suffix _ll indicates that there are two indices (the array is two-dimensional), both of which denote a local element node. We then initialize the system matrix and the right-hand side, initially with zeros:

1# System matrix, index g indicates global node
2system_matrix_gg = np.zeros([Nx*Ny, Nx*Ny])
3
4# Right hand side
5rhs_g = np.zeros(Nx*Ny)

The suffix _g denotes the index of the global node. The variable rhs_g contains the vector \(\v {f}\) and therefore needs only one index. The variable system_matrix_gg contains the system matrix \(\t {K}\) and therefore needs two global node indices.

10.4 System matrix

https://uni-freiburg.cloud.panopto.eu/Panopto/Pages/Embed.aspx?id=a6057226-fa98-45ed-a69f-acc000e9f3e7

The core of the simulation program is the structure of the system matrix. In this section, this is realized by explicit loops. The next section shows how this can be done in a more compact (and efficient) but less transparent way using special numpy commands.

First, we define a function that turns node coordinates into the global node index:

1def node_index(i, j, nb_nodes):
2 """
3 Turn node coordinates (i, j) into their global node index.
4
5 Parameters
6 ----------
7 i : int
8 x-coordinate (integer) of the node
9 j : int
10 y-coordinate (integer) of the node
11 nb_nodes : tuple of ints
12 Number of nodes in the Cartesian directions
13
14 Returns
15 -------
16 g : int
17 Global node index
18 """
19 Nx, Ny = nb_nodes
20 return i + Nx*j

We use this in another auxiliary function that adds the element matrix to the system matrix. To do this, the element matrix must first be stretched to the system matrix. The function looks like this:

1def add_element_matrix(system_matrix_gg, element_matrix_ll,
2 global_node_indices):
3 """
4 Add element matrix to global system matrix.
5
6 Parameters
7 ----------
8 system_matrix_gg : array_like
9 N x N system matrix where N is the number of global
10 nodes. This matrix will be modified by this function.
11 element_matrix_ll : array_like
12 n x n element matrix where n is the number of local
13 nodes
14 global_node_indices : list of int
15 List of length n that contains the global node
16 indices for the local node index that corresponds to
17 the list position.
18 """
19 assert element_matrix_ll.shape == \
20 (len(global_node_indices), len(global_node_indices))
21 for i in range(len(global_node_indices)):
22 for j in range(len(global_node_indices)):
23 system_matrix_gg[global_node_indices[i],
24 global_node_indices[j]] += \
25 element_matrix_ll[i, j]

The assert statement is a watchdog here, making sure that the local element matrix and the global_node_indices array have the same length. The two for loops then run through all the entries in the element matrix. The expression global_node_indices[i] then returns the global node index that corresponds to the local node index of the element matrix. The system matrix is then assembled by calling this auxiliary method for each element:

1def assemble_system_matrix(element_matrix_ll, nb_nodes):
2 """
3 Assemble system matrix from the element matrix
4
5 Parameters
6 ----------
7 element_matrix_ll : array_like
8 3 x 3 element matrix
9 nb_nodes : tuple of ints
10 Number of nodes in the Cartesian directions
11
12 Returns
13 -------
14 system_matrix_gg : numpy.ndarray
15 System matrix
16 """
17
18 Nx, Ny = nb_nodes
19 Mx, My = Nx-1, Ny-1 # number of boxes
20
21 # System matrix
22 system_matrix_gg = np.zeros([Nx*Ny, Nx*Ny])
23
24 # Construct system matrix
25 for l in range(Mx):
26 for m in range(My):
27 # Element (0)
28 n0 = node_index(l, m, nb_nodes)
29 n1 = node_index(l+1, m, nb_nodes)
30 n2 = node_index(l, m+1, nb_nodes)
31 add_element_matrix(system_matrix_gg,
32 element_matrix_ll,
33 [n0, n1, n2])
34
35 # Element (1)
36 n0 = node_index(l+1, m+1, nb_nodes)
37 n1 = node_index(l, m+1, nb_nodes)
38 n2 = node_index(l+1, m, nb_nodes)
39 add_element_matrix(system_matrix_gg,
40 element_matrix_ll,
41 [n0, n1, n2])
42
43 return system_matrix_gg

Here, the two for loops run over the individual boxes. The loop over the two elements per box is explicitly written as two calls to add_element_matrix. The variables n0, n1 and n2 contain the global node indices describing the corners of the respective element.

https://uni-freiburg.cloud.panopto.eu/Panopto/Pages/Embed.aspx?id=de8eb963-78bb-4fd1-8387-acc000eee353

The system matrix that has now been constructed has (implicitly) Neumann boundary conditions with \(\nabla \Phi \cdot \hat {n}(\v {r})=0\) on the boundary. We now have to add the Dirichlet conditions for the electrodes. To do this, we replace the rows of the system matrix and the corresponding entries of the load vector:

1def capacitor_bc(system_matrix_gg, rhs_g,
2 top_left, top_right, top_potential,
3 bottom_left, bottom_right, bottom_potential,
4 nb_nodes):
5 """
6 Set boundary conditions for the parallel plate capacitor.
7
8 Parameters
9 ----------
10 system_matrix_gg : numpy.ndarray
11 System matrix. The system matrix is modified by a call
12 to this function
13 rhs_g : numpy.ndarray
14 Right-hand side vector. The right-hand side vector is
15 modified by a call to this function.
16 top_left : int
17 Leftmost node of the top electrode
18 top_right : int
19 Rightmost node of the top electrode
20 top_potential : float
21 Electrostatic potential of the top electrode
22 bottom_left : int
23 Leftmost node of the bottom electrode
24 bottom_right : int
25 Rightmost node of the bottom electrode
26 bottom_potential : float
27 Electrostatic potential of the bottom electrode
28 nb_nodes : tuple of ints
29 Number of nodes in the Cartesian directions
30 """
31 Nx, Ny = nb_nodes
32 # Dirichlet boundary conditions for top plate
33 for i in range(top_left, top_right+1):
34 n = node_index(i, Ny-1, nb_nodes)
35 mat_g = np.zeros(Nx*Ny)
36 mat_g[n] = 1
37 system_matrix_gg[n] = mat_g
38 rhs_g[n] = top_potential
39
40 # Dirichlet boundary conditions for bottom plate
41 for i in range(bottom_left, bottom_right+1):
42 n = node_index(i, 0, nb_nodes)
43 mat_g = np.zeros(Nx*Ny)
44 mat_g[n] = 1
45 system_matrix_gg[n] = mat_g
46 rhs_g[n] = bottom_potential

https://uni-freiburg.cloud.panopto.eu/Panopto/Pages/Embed.aspx?id=cac3d182-6a34-4c9c-9b6c-acc000f19238

The entire simulation code now contains calls to these functions, followed by the numerical solution of the linear system of equations:

1# Construct system matrix
2system_matrix_gg = assemble_system_matrix(element_matrix_ll,
3 nb_nodes)
4
5# Boundary conditions
6capacitor_bc(system_matrix_gg, rhs_g,
7 top_left, top_right, top_potential,
8 bottom_left, bottom_right, bottom_potential,
9 nb_nodes)
10
11# Solve system of linear equations
12potential_g = np.linalg.solve(system_matrix_gg, rhs_g)

The variable potential_g now contains the values of the electrostatic potential on the nodes.

10.5 Visualization

https://uni-freiburg.cloud.panopto.eu/Panopto/Pages/Embed.aspx?id=61f0f7d8-e311-4dcf-9f01-acc000f4b8d4

The result of the calculation can be visualized using the matplotlib library. The function matplotlib.pyplot.tripcolor can plot data on a triangulated 2D grid. The following code block visualizes the result of the simulation using this function.

1import matplotlib.pyplot as plt
2import matplotlib.tri
3
4def make_grid(nb_nodes):
5 """
6 Make an array that contains all elements of the grid. The
7 elements are described by the global node indices of
8 their corners. The order of the corners is in order of
9 the local node index.
10
11 They are sorted in geometric positive order and the first
12 is the node with the right angle corner at the bottom
13 left. Elements within the same box are consecutive.
14
15 This is the first element per box:
16
17 2
18 | \
19 | \
20 dy | \
21 | \
22 0 --- 1
23
24 dx
25
26 This is the second element per box:
27
28 dx
29 1 ---0
30 \ |
31 \ | dy
32 \ |
33 \|
34 2
35
36 Parameters
37 ----------
38 nb_nodes : tuple of ints
39 Number of nodes in the Cartesian directions
40
41 Returns
42 -------
43 triangles_el : numpy.ndarray
44 Array containing the global node indices of the
45 element corners. The first index (suffix _e)
46 identifies the element number and the second index
47 (suffix _l) the local node index of that element.
48 """
49 Nx, Ny = nb_nodes
50 # These are the node position on a subsection of the grid
51 # that excludes the rightmost and topmost nodes. The
52 # suffix _G indicates this subgrid.
53 y_G, x_G = np.mgrid[:Ny-1, :Nx-1]
54 x_G.shape = (-1,)
55 y_G.shape = (-1,)
56
57 # List of triangles
58 lower_triangles = np.vstack(
59 (node_index(x_G, y_G, nb_nodes),
60 node_index(x_G+1, y_G, nb_nodes),
61 node_index(x_G, y_G+1, nb_nodes)))
62 upper_triangles = np.vstack(
63 (node_index(x_G+1, y_G+1, nb_nodes),
64 node_index(x_G, y_G+1, nb_nodes),
65 node_index(x_G+1, y_G, nb_nodes)))
66 # Suffix _e indicates global element index
67 return np.vstack(
68 (lower_triangles, upper_triangles)).T.reshape(-1, 3)
69
70def plot_results(values_g, nb_nodes, mesh_style=None,
71 ax=None):
72 """
73 Plot results of a finite-element calculation on a
74 two-dimensional structured grid using matplotlib.
75
76 Parameters
77 ----------
78 nb_nodes : tuple of ints
79 Number of nodes in the Cartesian directions
80 values_g : array_like
81 Expansion coefficients (values of the field) on the
82 global nodes
83 mesh_style : str, optional
84 Will show the underlying finite-element mesh with
85 the given style if set, e.g. ’ko-’ to see edges
86 and mark nodes by points
87 (Default: None)
88 ax : matplotlib.Axes, optional
89 Axes object for plotting
90 (Default: None)
91
92 Returns
93 -------
94 trim : matplotlib.collections.Trimesh
95 Result of tripcolor
96 """
97 Nx, Ny = nb_nodes
98
99 # These are the node positions on the full global grid.
100 y_g, x_g = np.mgrid[:Ny, :Nx]
101 x_g.shape = (-1,)
102 y_g.shape = (-1,)
103
104 # Gouraud shading linearly interpolates the color between
105 # the nodes
106 if ax is None:
107 ax = plt
108 triangulation = matplotlib.tri.Triangulation(
109 x_g, y_g, make_grid(nb_nodes))
110 c = ax.tripcolor(triangulation, values_g,
111 shading=gouraud)
112 if mesh_style is not None:
113 ax.triplot(triangulation, mesh_style)
114 return c
115
116 plt.subplot(111, aspect=1)
117 plot_results(potential_g, nb_nodes, show_mesh=True)
118 plt.xlabel(r$x$-position ($\Delta x$)’)
119 plt.ylabel(r$y$-position ($\Delta y$)’)
120 plt.colorbar().set_label(rPotential $\Phi$ (V)’)
121 plt.tight_layout()
122 plt.show()

The make_grid function here generates a list of global node indices per element. The first index is the index of the element (suffix e), the second index is the local node index within the element (suffix l). The nodes of the respective element are numbered counterclockwise. For visualization, “Gouraud” shading is used. This type of coloring linearly interpolates the value of the nodes on the triangles and exactly matches the interpolation rule of our shape functions. We can thus represent the full interpolated function \(\Phi _N(\v {r})\).

10.6 Example: Plate capacitor

With the help of the code developed here, the electrostatic potential within a plate capacitor can now be calculated. Figure 10.2 shows the result of this calculation for different resolutions of the simulation, i.e. different numbers of elements. By increasing the resolution, the simulation can be systematically improved.

PIC

Figure 10.2: Electrostatic potential within the plate capacitor, calculated with (a) \(4\times 4\) nodes (\(18\) elements) and (b) \(32\times 32\) nodes (\(1922\) elements). In (a) the color coding shows the linear function within the elements.

To calculate the capacitance, we now have to determine the charge on the capacitor plates. The total charge \(Q_\alpha \) on the electrode \(\alpha \) is obtained from the surface charge given by Eq. \eqref{eq:surfacecharge}. By integrating over the area of the capacitor plates \(A_\alpha \), we obtain \begin {equation} Q_\alpha = \int _{A_\alpha } \dif ^2 r\, \sigma (\v {r}) = \varepsilon \int _{A_\alpha } \dif ^2 r\, \nabla \Phi _N\cdot \hat {n}(\v {r}). \end {equation} Here, the permittivity \(\varepsilon \) plays an important role for the unit of the charge. We can now use the series expansion again. Only element type \((1)\) contributes to the integral, and here only the shape functions for which the derivative in the \(y\) direction does not vanish, since \(\nabla \Phi _N\cdot \hat {n}(\v {r})=\pm \partial \Phi _N/\partial y\). The sign is reversed for the upper and lower capacitor plates. Non-vanishing contributions come from the form functions \(N_0^{(1)}\) and \(N_2^{(1)}\). We get \begin {align} \int _0^{\Delta x} \dif x \frac {\partial N_0^{(1)}}{\partial y} &= \int _0^{\Delta x} \dif x \frac {1}{\Delta y} = \frac {\Delta x}{\Delta y} \\ \int _0^{\Delta x} \dif x \frac {\partial N_2^{(1)}}{\partial y} &= \int _0^{\Delta x} \dif x \left (-\frac {1}{\Delta y}\right ) = -\frac {\Delta x}{\Delta y} \end {align}

and thus for \(\Delta x=\Delta y\) \begin {equation} Q^{(n)} = \varepsilon t(a_0 - a_2) \end {equation} as the contribution of the element \((n)\) to the charge on the electrode. Here, the indices of the coefficients \(a_0\) and \(a_2\) denote the respective local node indices. The quantity \(t\) is the depth of the simulation domain. Since we are considering the problem here in two dimensions, all charges are effectively line charges (per depth) and the factor \(t\) is needed to obtain an absolute charge. Our plate capacitor is infinitely long in the third dimension. The factor \(\varepsilon t\) has the unit farad and is therefore a capacitance.

The capacitance of the capacitor is now given by \(C=Q_0/\Delta \Phi \), where \(Q_0\) is the charge on one capacitor plate and \(\Delta \Phi \) is the (given) potential difference. The second capacitor plate must carry the charge \(Q_1=-Q_0\). The code for calculating the charge on the capacitor plates therefore looks like this:

1def get_charge(potential_g, nb_nodes,
2top_left, top_right,
3 bottom_left, bottom_right):
4 """
5 Compute charge on both capacitor plates.
6
7 Parameters
8 ----------
9 potential_g : array_like
10 Electrostatic potential
11 nb_nodes : tuple of ints
12 Number of nodes in the Cartesian directions
13 top_left : int
14 Leftmost node of the top electrode
15 top_right : int
16 Rightmost node of the top electrode
17 bottom_left : int
18 Leftmost node of the bottom electrode
19 bottom_right : int
20 Rightmost node of the bottom electrode
21
22 Returns
23 -------
24 charge_top : float
25 Charge (divided by permittivity and thickness) on top
26 plate
27 charge_bottom : float
28 Charge (divided by permittivity and thickness) on
29 bottom plate
30 """
31 Nx, Ny = nb_nodes
32 charge_top = 0.0
33 for i in range(top_left+1, top_right+1):
34 charge_top += \
35 potential_g[node_index(i, Ny-1, nb_nodes)] - \
36 potential_g[node_index(i, Ny-2, nb_nodes)]
37
38 charge_bottom = 0.0
39 for i in range(bottom_left+1, bottom_right+1):
40 charge_bottom += \
41 potential_g[node_index(i, 0, nb_nodes)] - \
42 potential_g[node_index(i, 1, nb_nodes)]
43
44 return charge_top, charge_bottom

Of course, we know what the capacitance of a plate capacitor looks like. It is given by \begin {equation} C = \varepsilon \frac {A}{d}, \label {eq:anacapacity} \end {equation} where \(A=tL\) is the area of the capacitor plate and \(d\) is the distance between the plates. (\(L\) is the length of the plates, see Fig. 10.1a.) We can write this in dimensionless form as \begin {equation} \frac {C}{\varepsilon t} = \frac {L}{d}. \end {equation} We get the left side directly from our simulation. The result of the finite element calculation is shown in Fig. 10.3 compared with this analytical expression. You can see that the analytical expression only applies to small aspect ratios \(d/L<1\). The derivation of this expression assumes that the field lines are everywhere parallel and perpendicular to the capacitor plates. For large distances between the capacitor plates, this is no longer the case and stray fields at the edge of the plates begin to play a role in the capacitance. These are not included in Eq. \eqref{eq:anacapacity}, but are modeled in the simulation.

PIC

Figure 10.3: Capacitance \(C\) of a plate capacitor as a function of the distance between the plates \(d\). Both axes are de-dimensionalized and show quantities without units. The dashed line is the classical prediction for the capacity, the blue line shows the simulation. The simulation box was always chosen to be at least three times the plate length \(L\) or the distance between the plates \(d\). The plate length \(L\) was discretized with \(8\) elements.

Note: The system matrix of the finite element method is sparse. For sparse matrices, there are special data structures that simplify the handling of these matrices. These are implemented in the scipy.sparse package. We can use these routines to construct a sparse system matrix:

1from scipy.sparse import coo_matrix
2
3def assemble_system_matrix(element_matrix_ll, nb_nodes):
4 """
5 Assemble system matrix from the element matrix
6
7 Parameters
8 ----------
9 element_matrix_ll : array_like
10 3 x 3 element matrix
11 nb_nodes : tuple of ints
12 Number of nodes in the Cartesian directions
13
14 Returns
15 -------
16 system_matrix_gg : numpy.ndarray
17 System matrix
18 """
19 Nx, Ny = nb_nodes
20
21 # Compute grid
22 grid_el = make_grid(nb_nodes)
23
24 # Get number of elements
25 nb_elements, nb_corners = grid_el.shape
26
27 # Spread out grid and element matrix such that they can
28 # be used as global node coordinates for the sparse
29 # matrix
30 grid1_ell = np.stack(
31 [grid_el, grid_el, grid_el], axis=1)
32 grid2_ell = np.stack(
33 [grid_el, grid_el, grid_el], axis=2)
34 element_matrix_ell = np.stack(
35 [element_matrix_ll]*nb_elements, axis=0)
36
37 # Construct sparse system matrix
38 # ‘coo_matrix‘ will automatically sum duplicate entries
39 system_matrix_gg = coo_matrix(
40 (element_matrix_ell.reshape(-1),
41 (grid1_ell.reshape(-1), grid2_ell.reshape(-1))),
42 shape=(Nx*Ny, Nx*Ny))
43
44 return system_matrix_gg.todense()

This method replaces the above implementation of assemble_system_matrix. For reasons of compatibility with the rest of the implementation shown here, it returns a dense matrix at the end, but you can continue to work with the sparse matrix. As part of the application of coo_matrix, the global node indices are used here as the “coordinates” of the matrix entries. To do this, both the node indices and the entries of the element matrix must be multiplied by the size of the system matrix. This is done by the numpy.stack commands in these code fragments.

Bibliography


Copyright © 2017-2020 Andreas Greiner, 2020-2025 Lars Pastewka.