{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Simulating Poisson Networks Efficiently\n",
"\n",
"\n",
"When I first started my Ph.D. I learned about modeling large wireless network using point processes. They allow you to obtain tractable mathematical models that scale well with the size of the network. The most commonly used model in wireless networks is the homogeneous Poisson point proces (PPP), because it has a reasonable assumption about the network geometry and leads to tractable models. To briefly introduce the concept, say we have a set of two dimensional points $\\phi = \\{u_1, \\cdots\\}$ randomly distributed in $\\mathbb{R}^2$, such that for every compact set $B \\in \\mathbb{R}^2$. The set of points $\\phi$ is a HPPP if: \n",
"* The number of points of $\\phi$ in $B$ (denoted by $N(B)$) are poisson distributed with rate $\\lambda |B|$, where $\\lambda$ is the intensity of the HPPP and $|B|$ is the are of $B$.\n",
"* If $B_1$,$B_2$, ..., $B_m$ are all disjoint sets in $\\mathbb{R}^2$, then $N(B_1)$,$N(B_2)$, ..., $N(B_m)$ are independent random variables\n",
"\n",
"For more details on the theory behind this I recommending reading the book [*Stochastic Geometry for Wireless Networks*](https://www.amazon.ca/Stochastic-Geometry-Wireless-Networks-Professor/dp/1107014697/ref=sr_1_1?ie=UTF8&qid=1535753229&sr=8-1&keywords=stochastic+geometry+for+wireless+networks), by Martin Haenggi."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to simulate a HPPP, we determine an area of interest, say the ball $b(0,R)$, and generate $N$ points inside, where $N$ is drawn from a poisson distribution with rate $\\lambda \\pi R^2$. An example of a function to generate a realization of a HPPP can be seem below:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"def generate_PPP(lam, radius):\n",
" # Number of points inside a ball\n",
" N = np.random.poisson(lam * np.pi * radius**2)\n",
" # Distance from the origin\n",
" r = radius * np.sqrt(np.random.random(N))\n",
" # Angle from the origin\n",
" ang = 2 * np.pi * np.random.random(N)\n",
" # Matrix of the PPP\n",
" phi = np.vstack((r * np.cos(ang), r * np.sin(ang))).T\n",
" return phi"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Usually we run a simulation of a HPPP to validate the analytical results. When simulating Poisson networks, we are usually interested in averaging a performance metric, which depends on the distance distribution between the points of a HPPP and the origin or between two different HPPP's, through a Monte Carlo simulation. This means we have to calculate distances quite a few times. \n",
"\n",
"## Naive Distance Calculation\n",
"\n",
"When I first started simulating large networks, my simulations took a long time to run. Let's say you have to calculate the distances between two point processes $\\phi_1$ and $\\phi_2$ (e.g. a user point process and a base station point process), each with $N_1$ and $N_2$ points respectively. If you do it naively like I used to, you will have a nested for loop, looping through every point of each process, resulting in a complexity of $\\mathcal{O}(N_1 N_2)$, as the function shown below"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def naive_distance(phi_1, phi_2):\n",
" N1, _ = phi_1.shape\n",
" N2, _ = phi_2.shape\n",
" D = np.zeros((N1, N2))\n",
" for n1 in range(N1):\n",
" for n2 in range(N2):\n",
" D[n1, n2] = np.sqrt((phi_1[n1, 0] - phi_2[n2, 0])**2 + (phi_1[n1, 1] - phi_2[n2, 1])**2)\n",
" \n",
" return D"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## More Efficient Approach\n",
"\n",
"Once I started simulating HPPP's with higher densities this simulations started taking hours. I did some research on how to speed them up and figured out we can calculate these distances more efficiently. The distance between points $\\mathbf{x} \\in \\phi_1$ and $\\mathbf{y} \\in \\phi_2$ is given by\n",
"\n",
"\\begin{equation}\n",
"D_{\\mathbf{x}, \\mathbf{y}} = \\sqrt{(x_1 - y_1)^2 + (x_2 - y_2)^2} = \\sqrt{x_1^2 + x_2^2 + y_1^2 + y_2^2 - 2 (x_1 y_1 + x_2 y_2)}\n",
"\\end{equation}\n",
"\n",
"We can obtain a matrix $\\mathbf{D} \\in \\mathbb{R}^{N_1 \\times N_2}$ where each coordinate $D_{i, j}$ is the distance between the $i$-th point in $\\phi_1$ to the $j$-th point in $\\phi_2$ by\n",
"\n",
"\\begin{equation}\n",
"D = (\\mathbf{\\Phi_1} \\mathbf{1}_{2 \\times 1})^T \\mathbf{1}_{1 \\times N_2} + (\\mathbf{\\Phi_2} \\mathbf{1}_{2 \\times 1})^T \\mathbf{1}_{1 \\times N_1} - 2 \\mathbf{\\Phi_1} \\mathbf{\\Phi_2}^T\n",
"\\end{equation}\n",
"\n",
"Where $\\mathbf{\\Phi_1}$ and $\\mathbf{\\Phi_2}$ are a $N_1 \\times 2$ and a $N_2 \\times 2$ matrix respectively, where each row is a point from $\\phi_1$ and $\\phi_2$. The vector/matrix $\\mathbf{1}_{i \\times j}$ is a $i \\times j$ vector/matrix. This expression involves two matrix-vector multiplications, two inner products and one matrix-matrix multiplication. It might seem that the resulting complexity of this second algorithm is larger than the naive one, but processors and linear algebra libraries are optimized by experienced engineers to execute linear algebra operations really fast, which results in a smaller running time than the naive algorithm. \n",
"\n",
"A sample function that calculates the distance by this method is shown below"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def efficient_distance(phi_1, phi_2):\n",
" D = (np.sum(phi_1**2, axis=1).reshape((-1, 1))).dot(np.ones((1, phi_2.shape[0]))) + \\\n",
" (np.sum(phi_2**2, axis=1).reshape((-1, 1))).dot(np.ones((1, phi_1.shape[0]))).T - \\\n",
" 2 * phi_1.dot(phi_2.T)\n",
" D = np.sqrt(D)\n",
" \n",
" return D"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Comparison\n",
"\n",
"Now let's compare the running time of both functions"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Size of phi_1: (1250, 2)\n",
"Size of phi_2: (1316, 2)\n"
]
}
],
"source": [
"lam = 400\n",
"radius = 1\n",
"phi_1 = generate_PPP(lam, radius)\n",
"phi_2 = generate_PPP(lam, radius)\n",
"print(\"Size of phi_1: {0}\".format(phi_1.shape))\n",
"print(\"Size of phi_2: {0}\".format(phi_2.shape))"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5.14 s ± 493 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
]
}
],
"source": [
"%timeit d_naive = naive_distance(phi_1, phi_2)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"46.4 ms ± 1.43 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n"
]
}
],
"source": [
"%timeit d_eff = efficient_distance(phi_1, phi_2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As you can see, on average, the the efficient approach was 110.78 times faster than the naive one.\n",
"\n",
"You can download a Jupyter notebook with this tutorial [here](https://jvce92.github.io/files/simulating_poisson_networks.ipynb)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}