thesis.tex 20.7 KB
 sfritschi committed Oct 18, 2021 1 \documentclass[10pt]{article}  sfritschi committed Oct 14, 2021 2 3 4 5 6  \title{\textbf{Generation and Simulation of Large-Scale Flow Networks}} \author{Severin Fritschi} \date{}  sfritschi committed Oct 14, 2021 7 % packages  sfritschi committed Oct 18, 2021 8 \usepackage[backend=biber, sorting=none]{biblatex}  sfritschi committed Oct 21, 2021 9 \usepackage[labelfont=bf]{caption} % figure captions (bold-face)  sfritschi committed Oct 18, 2021 10 \usepackage{multicol} % multicolumn environment  sfritschi committed Oct 18, 2021 11 \usepackage{tikz} % figures  sfritschi committed Nov 30, 2021 12 \usepackage{xcolor} % color for cell lists picture  sfritschi committed Oct 21, 2021 13 14 \usepackage{hyperref} % web-links \usepackage{listings} % line break in verb environment  sfritschi committed Nov 28, 2021 15 16 17 18 19 20 21 \usepackage{amsmath} % math commands % packages for algorithms \usepackage{algorithm} \usepackage{algpseudocode} % argmin \DeclareMathOperator*{\argmin}{arg\min}  sfritschi committed Oct 14, 2021 22 23 24  % references \addbibresource{bib/RandomGenerationFlowNetworks.bib}  sfritschi committed Nov 27, 2021 25 \addbibresource{bib/netflow.bib}  sfritschi committed Oct 14, 2021 26 \addbibresource{bib/petsc.bib}  sfritschi committed Oct 18, 2021 27 \addbibresource{bib/hypre.bib}  sfritschi committed Oct 14, 2021 28   sfritschi committed Oct 14, 2021 29 30 \begin{document} \maketitle  sfritschi committed Oct 18, 2021 31 32  \begin{center} \large{Study Programme Computational Sciences and Engineering}  sfritschi committed Oct 14, 2021 33   sfritschi committed Oct 18, 2021 34 35  \vspace{20ex} \large{Bachelor Thesis HS 21}  sfritschi committed Oct 14, 2021 36   sfritschi committed Oct 18, 2021 37 38  \vspace{5ex} \large{Institute of Fluid Dynamics}  sfritschi committed Oct 14, 2021 39   sfritschi committed Oct 18, 2021 40  \large{ETH Zürich}  sfritschi committed Oct 14, 2021 41   sfritschi committed Oct 18, 2021 42  \vspace{20ex}  sfritschi committed Oct 14, 2021 43 44 45 46 47  \begin{tabular}{ l l } \large{Supervisor:} & \large{Daniel W. Meyer} \\ \large{Professor:} & \large{Patrick Jenny} \end{tabular} \end{center}  sfritschi committed Oct 18, 2021 48   sfritschi committed Oct 14, 2021 49 50  \newpage  sfritschi committed Oct 18, 2021 51  \begin{center} \Large{\textbf{Abstract}} \end{center}  sfritschi committed Nov 30, 2021 52  \hspace{0.5cm}To study the flow properties of large void-space geometries found in porous media such as f.ex. soil or gravel, \cite{MEYER2021103936} describes and implements routines for the generation \& simulation of flow networks in the Python library \emph{netflow} \cite{MEYER2021101592}. Based on a relatively small base network acquired via tomographic scans, the generated flow network is of intermediate size (millions of pores). To extend this procedure to even larger networks (up to 100 millions of pores), parallel computing is employed for both generation of pore-networks as well as solving the flow for said networks. In the latter, we rely on existing MPI-based parallel solvers from the PETSc \cite{petsc-web-page} toolkit. See Appendix ~\ref{appendix:install} for installation details.  sfritschi committed Oct 18, 2021 53 54 55 56  \vspace{5ex} \begin{multicols}{2} \section{Introduction}  sfritschi committed Nov 30, 2021 57  \hspace{0.5cm}Porous media are abundant in nature. Various types of soils harbor intricate networks that enable the flow of groundwater and subsequently the transport of important chemical compounds through the soil. To understand these natural phenomena, it is of key interest to study the flow through pore networks. However, this requires sufficiently large void-space geometries taken from porous bodies, which despite advances in scanning technologies, is infeasible. Additionally, in pursuit of simulation efficiency, the 3D images obtained from scans are converted into a simplified representation consisting of spherical pores (nodes), connected by cylindrical throats (edges). To overcome above size limitation, the paper from \cite{MEYER2021103936} outlines a procedure for generating random realizations of much larger flow networks, taking an existing base network (See Appendix ~\ref{appendix:base}), obtained from a scan of a porous medium, as input. Furthermore, this method is particularly useful for generating heterogeneous networks, characterized by an irregular pore distribution. This is done with dendrogram-based clustering of pores in the original network, followed by random rotations of said clusters. When performed repeatedly and arranged in a 3-dimensional grid of perturbed base network copies, the resulting network, carrying over pore statistics such as coordination number \& radius, preserves the irregular pore structure of the original network. The \emph{netflow} package that implements the mentioned algorithm also provides functionality for solving and analyzing the flow through such networks. The former of which is now to be parallelized in a distributed fashion, so that it may support larger networks with up to 100 millions of pores. In a further step, the network generation algorithm shall be complemented by a parallel shared memory approach, specifically for connecting the generated pores in the resulting network.  sfritschi committed Oct 18, 2021 58 59  \section{Parallel Flow Solver}  sfritschi committed Oct 18, 2021 60  \subsection{PETSc Interface}  sfritschi committed Oct 21, 2021 61  \hspace{0.5cm}Despite the availability of Python package \emph{petsc4py}, we decided to use the native implementation in C instead. This choice was motivated by the fact that the latter is better maintained, and the inclusion of an additional Python module would further complicate the dependency tree on the Python side. In order to interface the chosen C API of PETSc with the \emph{netflow} Python module, we rely on Cython to wrap the C source in Python, that is subsequently compiled with all required compilation and linking flags of PETSc. This allows us to invoke a \verb|solve_py()| function from Python delegating the relevant parameters, namely the system matrix and right-hand-side vector, to the C function \verb|solve()|. Here, the system matrix, in the compressed row storage format, is converted into PETSc's internal representation for sparse and distributed matrices called \verb|Mat|. The same applies to the r.h.s. which is used to initialize a distributed vector object in PETSc, i.e. \verb|Vec|. When the solution has been computed, it is communicated in full to the root rank via a call to \verb|MPI_Gatherv()|. It is stored in a Cython memoryview, converted back into a numpy array, and finally returned by \verb|solve_py()|.  sfritschi committed Oct 18, 2021 62  \subsection{Solver}  sfritschi committed Oct 21, 2021 63  \hspace{0.5cm}The actual solver written in C then utilizes PETSc's collection of krylov-subspace (KSP) methods to iteratively approximate the solution of the system in parallel with the available MPI processes. To avoid data duplication, the initial assembly of PETSc objects is only done on the root rank and then communicated in parts to the corresponding ranks through PETSc's collective \verb|Assembly| routines. The iterative method chosen to solve the non-symmetric pressure system, arising from the flow network, is GMRES together with a left algebraic multi-grid preconditioner supplied via hypre \cite{hypre-web-page}.  sfritschi committed Oct 21, 2021 64  \subsection{Limitations}  sfritschi committed Oct 21, 2021 65  \hspace{0.5cm}Since the datastructure used to represent the pores in \emph{netflow} is a Python \verb|set|, the order of the pores is arbitrary. In particular, this means each MPI process sees a different ordering from eachother, which necessitates initialization of the full system matrix \& r.h.s. on a given root rank, such that the ordering is consistent for all ranks. This requires additional communication, but prevents duplication of the data associated with the matrix etc. on the remaining processes.  sfritschi committed Oct 18, 2021 66  \subsection{Results}  sfritschi committed Oct 21, 2021 67  \hspace{0.5cm}In order to assess the quality of the pressure-solution obtained by this solver, we study the fluxes induced by the pore pressures for a given base network comprised of 2636 pores. In particular, we look at all in- and out-going fluxes as well as their sum on a per pore basis (except for source/sink pores), obtained from the function \verb|flux_balance()|. When we complete this analysis for all available solvers and compare the results of the parallel PETSc solver with the existing single-core solvers, we see that the parallel version is in complete agreement with the rest in terms of solution quality, as depicted in Figure ~\ref{fig:balance}.  sfritschi committed Oct 18, 2021 68 69  \end{multicols}  sfritschi committed Oct 18, 2021 70   sfritschi committed Nov 30, 2021 71 72  \begin{figure}[ht] \vspace{-0.5cm}  sfritschi committed Oct 18, 2021 73  \centering  sfritschi committed Oct 20, 2021 74  \includegraphics[width=0.8\textwidth]{plots/flux_PETSC.png}  sfritschi committed Oct 28, 2021 75  \caption{Pressures $p_{\mathrm{in}}$ and $p_{\mathrm{out}}$ are applied to in-pores and out-pores respectively, driving the network flow. The resulting pressure system is solved with the available solvers and the different fluxes are shown for the individual pores in the case of PETSc (using 4 processes). Since the sequential solvers produce an identical plot, they are omitted here. }  sfritschi committed Oct 18, 2021 76 77  \label{fig:balance} \end{figure}  sfritschi committed Nov 28, 2021 78   sfritschi committed Nov 27, 2021 79  \begin{multicols}{2}  sfritschi committed Nov 30, 2021 80 81  \section[Parallel Network Generation]{Parallel Network\\ Generation} \vspace{-0.25cm}  sfritschi committed Nov 28, 2021 82 83  \hspace{0.5cm}The existing serial dendrogram-based network generation algorithm, as presented in \cite{MEYER2021101592}, is now modified. Concretely, to allow connecting all pores that populate the larger, generated network domain in parallel, we rely on a shared memory approach via the \verb|multiprocessing| Python module. To accomplish this, we first shift our attention to \emph{cell lists}, which offer a direct application of the already existing \textbf{maximal throat length} parameter $L_m$ as a suitable \textbf{cell size}. \subsection{Cell Lists}  sfritschi committed Nov 30, 2021 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113  \hspace{0.5cm}To speed up neighbor search for the \emph{stationary} pores, we have opted to use the well-known cell lists data structure instead of the triangulation based approach outlined in \cite{MEYER2021103936}. This choice is supported by the useful properties of cell lists for our purpose of generating a network of similar pore-arrangement. It is also favorable over the triangulation method as the cell lists are only initialized once, and pores that have already been processed by the algorithm can be removed efficiently. Primarily however, it allows a straight-forward application of parallel computing, by distributing the work needed to find the neighbors of individual pores evenly. See Figure ~\ref{fig:cell} for a visualization. \end{multicols} \newpage \begin{figure}[ht] \vspace{-0.5cm} \centering \begin{tikzpicture} \draw[black] (-3,-3) grid (3,3); \draw[draw=orange, fill=orange!10!white] (-3, -3) grid (2, -2) rectangle (-3, -3); \draw[draw=orange, fill=orange!10!white] (2, -3) grid (3, 3) rectangle (2, -3); \draw[draw=orange, fill=orange!10!white] (-3, 2) grid (2, 3) rectangle (-3, 2); \draw[draw=orange, fill=orange!10!white] (-3, -2) grid (-2, 2) rectangle (-3, -2); \draw[draw=blue, fill=blue!10!white] (-2, -1) grid (1, 2) rectangle (-2, -1); \node[anchor=north, black] at (-2.5, -3) {$0$}; \node[anchor=north, black] at (-1.5, -3) {$1$}; \node[anchor=north, black] at (-0.5, -3) {$2$}; \node[anchor=north, black] at (0.5, -3) {$3$}; \node[anchor=north, black] at (1.5, -3) {$4$}; \node[anchor=north, black] at (2.5, -3) {$5$}; \node[anchor=east, black] at (-3, -2.5) {$0$}; \node[anchor=east, black] at (-3, -1.5) {$1$}; \node[anchor=east, black] at (-3, -0.5) {$2$}; \node[anchor=east, black] at (-3, 0.5) {$3$}; \node[anchor=east, black] at (-3, 1.5) {$4$}; \node[anchor=east, black] at (-3, 2.5) {$5$}; % Points \node[red] at (-0.5, 0.5) {\textbullet};  sfritschi committed Dec 03, 2021 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206  \node[black] at (-1.04, 1.78) {\textbullet}; \node[black] at (2.96, 1.78) {\textbullet}; \node[black] at (-1.04, -2.22) {\textbullet}; \node[black] at (2.96, -2.22) {\textbullet}; \node[black] at (-0.10, 0.20) {\textbullet}; \node[black] at (-0.13, -0.03) {\textbullet}; \node[black] at (0.44, 0.23) {\textbullet}; \node[black] at (-0.02, 1.70) {\textbullet}; \node[black] at (-0.02, -2.30) {\textbullet}; \node[black] at (-1.33, 1.17) {\textbullet}; \node[black] at (2.67, 1.17) {\textbullet}; \node[black] at (-1.33, -2.83) {\textbullet}; \node[black] at (2.67, -2.83) {\textbullet}; \node[black] at (0.63, -0.33) {\textbullet}; \node[black] at (-1.05, 1.71) {\textbullet}; \node[black] at (2.95, 1.71) {\textbullet}; \node[black] at (-1.05, -2.29) {\textbullet}; \node[black] at (2.95, -2.29) {\textbullet}; \node[black] at (-1.33, 0.53) {\textbullet}; \node[black] at (2.67, 0.53) {\textbullet}; \node[black] at (-0.86, 0.14) {\textbullet}; \node[black] at (-1.43, -0.91) {\textbullet}; \node[black] at (2.57, -0.91) {\textbullet}; \node[black] at (0.20, 1.58) {\textbullet}; \node[black] at (0.20, -2.42) {\textbullet}; \node[black] at (1.08, 1.53) {\textbullet}; \node[black] at (-2.92, 1.53) {\textbullet}; \node[black] at (1.08, -2.47) {\textbullet}; \node[black] at (-2.92, -2.47) {\textbullet}; \node[black] at (-0.83, -1.07) {\textbullet}; \node[black] at (-0.83, 2.93) {\textbullet}; \node[black] at (1.51, -0.19) {\textbullet}; \node[black] at (-2.49, -0.19) {\textbullet}; \node[black] at (1.25, 0.25) {\textbullet}; \node[black] at (-2.75, 0.25) {\textbullet}; \node[black] at (0.24, -1.89) {\textbullet}; \node[black] at (0.24, 2.11) {\textbullet}; \node[black] at (1.64, -1.59) {\textbullet}; \node[black] at (-2.36, -1.59) {\textbullet}; \node[black] at (1.64, 2.41) {\textbullet}; \node[black] at (-2.36, 2.41) {\textbullet}; \node[black] at (0.81, -0.14) {\textbullet}; \node[black] at (0.99, 1.11) {\textbullet}; \node[black] at (0.99, -2.89) {\textbullet}; \node[black] at (1.56, 0.19) {\textbullet}; \node[black] at (-2.44, 0.19) {\textbullet}; \node[black] at (0.00, 0.29) {\textbullet}; \node[black] at (-0.59, 1.98) {\textbullet}; \node[black] at (-0.59, -2.02) {\textbullet}; \node[black] at (0.69, 0.22) {\textbullet}; \node[black] at (1.72, -0.51) {\textbullet}; \node[black] at (-2.28, -0.51) {\textbullet}; \node[black] at (1.46, 0.32) {\textbullet}; \node[black] at (-2.54, 0.32) {\textbullet}; \node[black] at (-1.95, -1.76) {\textbullet}; \node[black] at (2.05, -1.76) {\textbullet}; \node[black] at (-1.95, 2.24) {\textbullet}; \node[black] at (2.05, 2.24) {\textbullet}; \node[black] at (-1.17, 0.31) {\textbullet}; \node[black] at (2.83, 0.31) {\textbullet}; \node[black] at (0.55, -0.60) {\textbullet}; \node[black] at (0.38, -1.83) {\textbullet}; \node[black] at (0.38, 2.17) {\textbullet}; \node[black] at (-0.87, 1.61) {\textbullet}; \node[black] at (-0.87, -2.39) {\textbullet}; \node[black] at (-0.72, -1.95) {\textbullet}; \node[black] at (-0.72, 2.05) {\textbullet}; \node[black] at (0.23, -0.43) {\textbullet}; \node[black] at (-1.72, 1.11) {\textbullet}; \node[black] at (2.28, 1.11) {\textbullet}; \node[black] at (-1.72, -2.89) {\textbullet}; \node[black] at (2.28, -2.89) {\textbullet}; \node[black] at (-0.03, -0.16) {\textbullet}; \node[black] at (1.77, 1.06) {\textbullet}; \node[black] at (-2.23, 1.06) {\textbullet}; \node[black] at (1.77, -2.94) {\textbullet}; \node[black] at (-2.23, -2.94) {\textbullet}; \node[black] at (-0.79, 0.18) {\textbullet}; \node[black] at (-1.34, -0.35) {\textbullet}; \node[black] at (2.66, -0.35) {\textbullet}; \node[black] at (-1.77, -1.49) {\textbullet}; \node[black] at (2.23, -1.49) {\textbullet}; \node[black] at (-1.77, 2.51) {\textbullet}; \node[black] at (2.23, 2.51) {\textbullet}; \node[black] at (-0.13, -0.43) {\textbullet}; \node[black] at (-0.28, 0.22) {\textbullet}; \node[black] at (1.69, 0.74) {\textbullet}; \node[black] at (-2.31, 0.74) {\textbullet}; \node[black] at (0.16, -0.40) {\textbullet}; \node[black] at (1.73, -1.17) {\textbullet}; \node[black] at (-2.27, -1.17) {\textbullet}; \node[black] at (1.73, 2.83) {\textbullet}; \node[black] at (-2.27, 2.83) {\textbullet};  sfritschi committed Nov 30, 2021 207 208 209 210  \end{tikzpicture} \caption{Cell lists visualized in 2D. The neighborhood of the pore highlighted in red is marked in blue. Because the cell-size is the maximally permissible throat length $L_m$, only the pores contained within the blue region must be considered during neighbor search. Finally, the periodic buffer layers, containing copies of pores on the opposite side from the interior, are painted in orange.} \label{fig:cell} \end{figure}  sfritschi committed Nov 28, 2021 211   sfritschi committed Nov 30, 2021 212  \begin{multicols}{2}  sfritschi committed Nov 28, 2021 213 214 215 216 217  \subsection{Iterative Algorithm} \hspace{0.5cm}To connect the generated pores by throats, we employ an \textbf{iterative} strategy. Beforehand however, the cell list is constructed based on the extent of the full domain (including periodic buffer layer) and cell-size $L_m$. Next, all pores are placed in their respective cell computed from their position. Now, for each pore and for each of its throats, which are copied from the base network and are sought to be realized, we find an \emph{ideal} match from its \textbf{neighboring cells}. Here, ideal refers to minimal absolute difference between physical distance of the pores and original length of the current throat $L_t$. Given position of $i$-th pore $\mathbf{p}_i$ and original throat length $L_t$ we seek: j^* = \argmin_{j \in \mathcal{N}(i)}\Bigr| ||\mathbf{p}_i - \mathbf{p}_j||_2 - L_t \Bigr|  sfritschi committed Dec 03, 2021 218 219 220  Where $\mathcal{N}(i)$ is the set of all pores in adjacent cells to pore $i$. Finding $j^*$ for different pores is \emph{embarrassingly parallel} and can therefore be computed by a large number of threads, storing their results in shared memory. Each thread works on an even chunk of the pores located \emph{inside} the domain. Subsequently, these ideal matches are connected, while avoiding \textbf{conflicts} of pores seeking either a neighbor that is already fully-connected or that was previously connected to them. Finally we repeat the two steps from above, now only considering pores that still have throats left in \emph{random} order (for improved \textbf{load balancing}), until \textbf{no more connections} can be found. Empirically, this process converges very fast and consistently, for different target sizes of the full domain, after merely 7-8 iterations. The first iteration alone leaves only $\approx 18\%$ of all possible throats left, see Table ~\ref{table:iter}. Final iterations may be performed serially if only few pores remain as to avoid costly spawning of threads without speed gain. The procedure is summarized as pseudo-code in Algorithm ~\ref{alg:connect}. \subsection{Results} \hspace{0.5cm}The parallel algorithm achieves  sfritschi committed Nov 27, 2021 221 222  \end{multicols}  sfritschi committed Nov 30, 2021 223  \begin{algorithm}[ht]  sfritschi committed Nov 28, 2021 224 225 226 227 228  \caption{Connect pores in parallel} \begin{algorithmic} \State Initialize $cellList$ using $pores$ and compute $totalThroats$ \State $poresRemain \gets pores$ \State $throatsLeft \gets totalThroats$  sfritschi committed Dec 03, 2021 229 230  \State $throatsUnrealized \gets totalThroats$ \While{$throatsUnrealized > 0$}  sfritschi committed Nov 28, 2021 231 232 233 234 235 236  \State Spawn $nthreads$ threads \State Compute best matches for all pores in $poresRemain$ using threads \State Store result in shared memory location $poreMatchTable$ \For{$pore$ \textbf{in} $poresRemain$} \For{$throat$ \textbf{in} $pore.throats$} \State Fetch $match$ pore from $poreMatchTable$  sfritschi committed Nov 30, 2021 237  \If{$throat$ is not already realized \textbf{and} $match$ is found}  sfritschi committed Nov 28, 2021 238 239 240 241 242 243 244 245 246 247 248 249 250  \State Realize $throat$ \EndIf \EndFor \EndFor \State Compute list of pores with throats left in random order: $nextPores$ \State $poresRemain \gets nextPores$ \State Count throats that are still left: $nextThroats$ \State $throatsLeft \gets nextThroats$ \EndWhile \end{algorithmic} \label{alg:connect} \end{algorithm}  sfritschi committed Nov 30, 2021 251 252  \begin{table}[ht] \centering  sfritschi committed Dec 03, 2021 253  \caption{Sample run of parallel pore-connecting algorithm using 4 threads. The generated network is 3 times as large as the base network in all directions. The maximal feasible number of throats is 103464, of which 27 were not realized due to there being no possible candidates left for these remaining pores.}  sfritschi committed Nov 30, 2021 254 255 256 257  \begin{tabular}{|c|c|c|} \hline \textbf{Iteration} & \textbf{Throats left} & \textbf{Rel. percentage} \\ \hline  sfritschi committed Dec 03, 2021 258 259 260 261 262 263 264 265  0 & 103464 & 100.0\% \\ 1 & 18690 & 18.1\% \\ 2 & 4329 & 4.2\% \\ 3 & 1096 & 1.1\% \\ 4 & 282 & 0.3\% \\ 5 & 73 & 0.1\% \\ 6 & 30 & 0.03\% \\ 7 & 27 & 0.026\% \\  sfritschi committed Nov 30, 2021 266 267 268 269 270  \hline \end{tabular} \label{table:iter} \end{table}  sfritschi committed Oct 20, 2021 271 272  \newpage  sfritschi committed Oct 21, 2021 273 274 275  \appendix \begin{center} \Large{\textbf{Appendix}} \end{center} \section{PETSc Installation}  sfritschi committed Oct 21, 2021 276  \label{appendix:install}  sfritschi committed Nov 28, 2021 277  For the purposes of this thesis, PETSc was installed in the following way. Given \textbf{existing} (open)MPI compilers located at \verb|/usr/bin/| (Linux).  sfritschi committed Oct 21, 2021 278 279 280 281 282 283 284 285 286 287 288 289  \begin{itemize} \item Clone \href{https://gitlab.com/petsc/petsc}{PETSc repository}. \item Run \textbf{configuration} script:\\ \begin{tabular}{ l l } \verb|./configure| & \verb|--with-cc=/usr/bin/mpicc| \\ & \verb|--with-cxx=/usr/bin/mpicxx| \\ & \verb|--with-fc=0| \\ & \verb|--download-hypre| \end{tabular} \item Set environment flags \verb|PETSC_DIR| and \verb|PETSC_ARCH| as specified in the output of \verb|./configure| \& run \textbf{make all}. \end{itemize}  sfritschi committed Nov 30, 2021 290 291  \section{Base Network} \label{appendix:base}  sfritschi committed Dec 03, 2021 292 293 294 295 296 297 298 299 300 301  Throughout this thesis we rely on existing networks obtained via tomographic scans to serve as a \emph{basis} for \textbf{generation} of larger networks and \textbf{simulation} of network flow. The statistics of the base network mentioned previously are detailed in Figure ~\ref{fig:base}. \begin{figure}[ht] \vspace{-0.5cm} \centering \includegraphics[width=0.8\textwidth]{plots/base.png} \caption{Network consisting of 2636 pores and 4291 throats, inscribed within a cube extending $1.07\cdot 10^{-3}$m in each spatial direction. The pore-arrangement is obviously \emph{not uniform}, as can be seen by the clustering of pores in some regions, while others are mostly undisturbed. The \emph{porosity}, measured as the relative fraction of volume taken up by the void-space geometry, is roughly $32\%$.} \label{fig:base} \end{figure}  sfritschi committed Oct 21, 2021 302 303  \newpage  sfritschi committed Oct 18, 2021 304  \centering  sfritschi committed Oct 14, 2021 305  \printbibliography  sfritschi committed Oct 14, 2021 306 \end{document}