├── .gitignore ├── Benchmarks.ipynb ├── LICENSE ├── NUFFT-Numba.ipynb ├── README.md ├── nufftpy ├── __init__.py ├── _numba_tools.py ├── nufft.py └── tests │ ├── __init__.py │ └── test_nufft1d.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | .ipynb_checkpoints 4 | 5 | *.pyc 6 | 7 | build -------------------------------------------------------------------------------- /Benchmarks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "", 4 | "signature": "sha256:c06499ba1fa9759a13111e7fff4f82eab9d43a509c8d4950f93349c03c046203" 5 | }, 6 | "nbformat": 3, 7 | "nbformat_minor": 0, 8 | "worksheets": [ 9 | { 10 | "cells": [ 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "# Benchmarks\n", 16 | "\n", 17 | "This script shows benchmarks of the Non-Uniform Fast Fourier Transform (NUFFT). There is a Fortran implementation of the NUFFT (python wrappers at http://github.com/dfm/python-nufft/) and the pure-Python implementation of NUFFT (http://github.com/jakevdp/nufftpy/).\n", 18 | "Both are $O[N\\log N]$ for $N$ observations and $N$ frequencies, but the fortran version is about two times faster than the pure Python version." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "collapsed": false, 24 | "input": [ 25 | "%matplotlib inline\n", 26 | "import numpy as np\n", 27 | "import matplotlib.pyplot as plt\n", 28 | "import seaborn; seaborn.set()" 29 | ], 30 | "language": "python", 31 | "metadata": {}, 32 | "outputs": [], 33 | "prompt_number": 1 34 | }, 35 | { 36 | "cell_type": "code", 37 | "collapsed": false, 38 | "input": [ 39 | "import nufft\n", 40 | "help(nufft.nufft1)" 41 | ], 42 | "language": "python", 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "output_type": "stream", 47 | "stream": "stdout", 48 | "text": [ 49 | "Help on function nufft1 in module nufft.nufft:\n", 50 | "\n", 51 | "nufft1(x, y, ms, df=1.0, eps=1e-15, iflag=1, direct=False)\n", 52 | "\n" 53 | ] 54 | } 55 | ], 56 | "prompt_number": 2 57 | }, 58 | { 59 | "cell_type": "code", 60 | "collapsed": false, 61 | "input": [ 62 | "import nufftpy\n", 63 | "help(nufftpy.nufft1)" 64 | ], 65 | "language": "python", 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "output_type": "stream", 70 | "stream": "stdout", 71 | "text": [ 72 | "Help on function nufft1 in module nufftpy.nufft:\n", 73 | "\n", 74 | "nufft1(x, c, M, df=1.0, eps=1e-15, iflag=1, direct=False, fast_gridding=True, use_numba=True)\n", 75 | " Fast Non-Uniform Fourier Transform (Type 1: uniform frequency grid)\n", 76 | " \n", 77 | " Compute the non-uniform FFT of one-dimensional points x with complex\n", 78 | " values c. Result is computed at frequencies (df * m)\n", 79 | " for integer m in the range -M/2 < m < M/2.\n", 80 | " \n", 81 | " Parameters\n", 82 | " ----------\n", 83 | " x, c : array_like\n", 84 | " real locations x and complex values c of the points to be transformed.\n", 85 | " M, df : int & float\n", 86 | " Parameters specifying the desired frequency grid. Transform will be\n", 87 | " computed at frequencies df * (-(M//2) + arange(M))\n", 88 | " eps : float\n", 89 | " The desired approximate error for the FFT result. Must be in range\n", 90 | " 1E-33 < eps < 1E-1, though be aware that the errors are only well\n", 91 | " calibrated near the range 1E-12 ~ 1E-6. eps is not referenced if\n", 92 | " direct = True.\n", 93 | " iflag : float\n", 94 | " if iflag<0, compute the transform with a negative exponent.\n", 95 | " if iflag>=0, compute the transform with a positive exponent.\n", 96 | " direct : bool (default = False)\n", 97 | " If True, then use the slower (but more straightforward)\n", 98 | " direct Fourier transform to compute the result.\n", 99 | " fast_gridding : bool (default = True)\n", 100 | " If True, use the fast Gaussian grid algorithm of Greengard & Lee (2004)\n", 101 | " Otherwise, use a more naive gridding approach\n", 102 | " use_numba : bool (default = True)\n", 103 | " If True, use numba to compute the result. If False or if numba is not\n", 104 | " installed, default to the numpy version, which is ~5x slower.\n", 105 | " \n", 106 | " Returns\n", 107 | " -------\n", 108 | " Fk : ndarray\n", 109 | " The complex discrete Fourier transform\n", 110 | " \n", 111 | " See Also\n", 112 | " --------\n", 113 | " nufftfreqs : compute the frequencies of the nufft results\n", 114 | "\n" 115 | ] 116 | } 117 | ], 118 | "prompt_number": 3 119 | }, 120 | { 121 | "cell_type": "code", 122 | "collapsed": false, 123 | "input": [ 124 | "M = 100000\n", 125 | "x = 100 * np.random.random(M)\n", 126 | "c = np.exp(1j * x)\n", 127 | "\n", 128 | "kwds = dict(eps=1E-8, iflag=-1, direct=False)\n", 129 | "\n", 130 | "k1 = nufft.nufft1freqs(M)\n", 131 | "F1 = nufft.nufft1(x, c, M, **kwds)\n", 132 | "\n", 133 | "k2 = nufftpy.nufftfreqs(M)\n", 134 | "F2 = nufftpy.nufft1(x, c, M, **kwds)\n", 135 | "\n", 136 | "print(np.allclose(k1, k2))\n", 137 | "print(np.allclose(F1, F2, atol=1E-8))" 138 | ], 139 | "language": "python", 140 | "metadata": {}, 141 | "outputs": [ 142 | { 143 | "output_type": "stream", 144 | "stream": "stdout", 145 | "text": [ 146 | "True\n", 147 | "True\n" 148 | ] 149 | } 150 | ], 151 | "prompt_number": 4 152 | }, 153 | { 154 | "cell_type": "code", 155 | "collapsed": false, 156 | "input": [ 157 | "Mrange = (2 ** np.arange(3, 21)).astype(int)\n", 158 | "kwds = dict(eps=1E-8, iflag=-1, direct=False)\n", 159 | "\n", 160 | "nufft_times = []\n", 161 | "nufftpy_times = []\n", 162 | "\n", 163 | "for M in Mrange:\n", 164 | " x = 100 * np.random.random(M)\n", 165 | " c = np.exp(1j * x)\n", 166 | " \n", 167 | " t1 = %timeit -oq nufft.nufft1(x, c, M, **kwds)\n", 168 | " t2 = %timeit -oq nufftpy.nufft1(x, c, M, **kwds)\n", 169 | " \n", 170 | " nufft_times.append(t1.best)\n", 171 | " nufftpy_times.append(t2.best)" 172 | ], 173 | "language": "python", 174 | "metadata": {}, 175 | "outputs": [], 176 | "prompt_number": 5 177 | }, 178 | { 179 | "cell_type": "code", 180 | "collapsed": false, 181 | "input": [ 182 | "plt.loglog(Mrange, nufftpy_times, label='nufft python')\n", 183 | "plt.loglog(Mrange, nufft_times, label='nufft fortran')\n", 184 | "plt.legend(loc='upper left')\n", 185 | "plt.xlabel('Number of Elements')\n", 186 | "plt.ylabel('Execution Time (s)');" 187 | ], 188 | "language": "python", 189 | "metadata": {}, 190 | "outputs": [ 191 | { 192 | "metadata": {}, 193 | "output_type": "display_data", 194 | "png": "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\n", 195 | "text": [ 196 | "" 197 | ] 198 | } 199 | ], 200 | "prompt_number": 6 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": {}, 205 | "source": [ 206 | "For large inputs, the pure-Python version is less than a factor of 2 slower than the optimized Fortran equivalent. Pretty good!" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | " " 214 | ] 215 | } 216 | ], 217 | "metadata": {} 218 | } 219 | ] 220 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Jake Vanderplas 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /NUFFT-Numba.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "", 4 | "signature": "sha256:f51281b869710141b6cac2fac47649b120ab662ed2a931ac2ed8122d0c2aa805" 5 | }, 6 | "nbformat": 3, 7 | "nbformat_minor": 0, 8 | "worksheets": [ 9 | { 10 | "cells": [ 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "# Optimizing Python in the Real World: NumPy, Numba, & the NUFFT" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "*This notebook originally appeared as a [post](http://jakevdp.github.io/blog/2015/02/24/optimizing-python-with-numpy-and-numba/) on the blog [Pythonic Perambulations](http://jakevdp.github.io). The content is BSD licensed.\n", 23 | "A github repo with some of the code appearing below can be found at http://github.com/jakevdp/nufftpy/.*" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "\n", 31 | "\n", 32 | "Donald Knuth famously quipped that \"premature optimization is the root of all evil.\"\n", 33 | "The reasons are straightforward: optimized code tends to be much more difficult to read and debug than simpler implementations of the same algorithm, and optimizing too early leads to greater costs down the road.\n", 34 | "In the Python world, there is another cost to optimization: optimized code often is written in a compiled language like Fortran or C, and this leads to barriers to its development, use, and deployment.\n", 35 | "\n", 36 | "Too often, tutorials about optimizing Python use trivial or toy examples which may not map well to the real world.\n", 37 | "I've certainly been [guilty](https://jakevdp.github.io/blog/2012/08/24/numba-vs-cython/) of this [myself](https://jakevdp.github.io/blog/2013/06/15/numba-vs-cython-take-2/).\n", 38 | "Here, I'm going to take a different route: in this post I will outline the process of understanding, implementing, and optimizing a non-trivial algorithm in Python, in this case the [Non-uniform Fast Fourier Transform](http://www.cims.nyu.edu/cmcl/nufft/nufft.html) (NUFFT).\n", 39 | "Along the way, we'll dig into the process of optimizing Python code, and see how a relatively straightforward pure Python implementation, with a little help from [Numba](http://numba.pydata.org), can be made to nearly match the performance of a highly-optimized Fortran implementation of the same algorithm.\n", 40 | "\n", 41 | "" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "## Why a Python Implementation?\n", 49 | "\n", 50 | "First, I want to answer the inevitable question: why spend the time to make a Python implementation of an algorithm that's already out there in Fortran?\n", 51 | "The reason is that I've found in my research and teaching that pure-Python implementations of algorithms are far more valuable than C or Fortran implementations, even if they might be a bit slower.\n", 52 | "This is for a number of reasons:\n", 53 | "\n", 54 | "- **Pure-Python code is easier to read, understand, and contribute to.** Good Python implementations are much higher-level than C or Fortran, and abstract-away loop indices, bit twiddling, workspace arrays, and other sources of code clutter. A typical student reading good Python code can immediately understand and modify the algorithm, while the same student would be lost trying to understand typical optimized Fortran code.\n", 55 | "\n", 56 | "- **Pure-python packages are much easier to install than Python-wrapped C or Fortran code.** This is especially true on non-Linux systems. Fortran in particular can require some installation prerequisites that are non-trivial for many users. In practice, I've seen people give up on better tools when there is an installation barrier for those tools.\n", 57 | "\n", 58 | "- **Pure-python code often works for many data types.** Because of the way it is written, pure Python code is often automatically applicable to single or double precision, and perhaps even to extensions to complex numbers. For compiled packages, supporting and compiling for all possible types can be a burden.\n", 59 | "\n", 60 | "- **Pure-python is easier to use at scale.** Because it does not require complicated installation, pure Python packages can be much easier to install on cloud VMs and/or shared clusters for computation at scale. If you can easily pip-install a pure-Python package on a VM, then services like AWS and TravisCI are much easier to set up.\n", 61 | "\n", 62 | "Certainly code speed will overcome these considerations if the performance gap is great enough, but I've found that for many applications a pure Python package, cleverly designed and optimized, can be made fast enough that these larger considerations win-out. The challenge is making the Python fast. We'll explore this below." 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## Background: The Non-Uniform Fast Fourier Transform\n", 70 | "\n", 71 | "The Fast Fourier Transform (FFT) is perhaps the most important and fundamental of modern numerical algorithms.\n", 72 | "It provides a fast, $O[N\\log N]$ method of computing the discrete Fourier transform:\n", 73 | "$$\n", 74 | "Y_k^\\pm = \\sum_{n=0}^{N-1} y_n e^{\\pm i k n / N}\n", 75 | "$$\n", 76 | "You can read more about the FFT in [my previous post](https://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/) on the subject.\n", 77 | "\n", 78 | "One important limitation of the FFT is that it requires that input data be evenly-spaced: that is, we can think of the values $y_n$ as samples of a function $y_n = y(x_n)$ where $x_n = x_0 + n\\Delta x$ is a regular grid of points.\n", 79 | "But what about when your grid is not uniform?\n", 80 | "That is, what if you want to compute this result:\n", 81 | "$$\n", 82 | "Y_k^\\pm = \\sum_{j=1}^N y(x_j) e^{\\pm i k x_j}\n", 83 | "$$\n", 84 | "where $y(x)$ is evaluated at an arbitrary set of points $x_j$?\n", 85 | "In this case, the FFT is no longer directly applicable, and you're stuck using a much slower $O[N^2]$ direct summation.\n", 86 | "\n", 87 | "Stuck, that is, until the NUFFT came along.\n", 88 | "\n", 89 | "The NUFFT is a clever algorithm which converts the non-uniform transform into an approximate uniform transform, not with error-prone interpolation, but instead using a clever \"gridding\" operation motivated by the convolution theorem.\n", 90 | "If you'd like to read about the algorithm in detail, the Courant Institute's [NUFFT page](http://www.cims.nyu.edu/cmcl/nufft/nufft.html) has a nice set of resources.\n", 91 | "\n", 92 | "Below we'll take a look at implementing this algorithm in Python." 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "## Direct Non-Uniform Fourier Transform\n", 100 | "\n", 101 | "When developing optimized code, it is important to start with something easy to make sure you're on the right track.\n", 102 | "Here we'll start with a straightforward direct version of the non-uniform Fourier transform.\n", 103 | "We'll allow non-uniform inputs $x_j$, but compute the output on a grid of $M$ evenly-spaced frequencies in the range $-M/2 \\le f/\\delta f < M/2$.\n", 104 | "This is what the NUFFT group calls the *Type-1 NUFFT*.\n", 105 | "\n", 106 | "First we'll implement ``nufftfreqs()``, which returns the frequency grid for a given $M$, and ``nudft()`` which computes the non-uniform discrete Fourier transform using a slow direct method. The arguments for the latter include ``iflag``, which is a positive or negative number indicating the desired sign of the exponent:" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "collapsed": false, 112 | "input": [ 113 | "from __future__ import print_function, division\n", 114 | "import numpy as np\n", 115 | "\n", 116 | "def nufftfreqs(M, df=1):\n", 117 | " \"\"\"Compute the frequency range used in nufft for M frequency bins\"\"\"\n", 118 | " return df * np.arange(-(M // 2), M - (M // 2))\n", 119 | "\n", 120 | "\n", 121 | "def nudft(x, y, M, df=1.0, iflag=1):\n", 122 | " \"\"\"Non-Uniform Direct Fourier Transform\"\"\"\n", 123 | " sign = -1 if iflag < 0 else 1\n", 124 | " return (1 / len(x)) * np.dot(y, np.exp(sign * 1j * nufftfreqs(M, df) * x[:, np.newaxis]))" 125 | ], 126 | "language": "python", 127 | "metadata": {}, 128 | "outputs": [] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "Again, I can't emphasize this enough: when writing fast code, start with a slow-and-simple version of the code which you *know* gives the correct result, and then optimize from there." 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "## Comparing to the Fortran NUFFT" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "metadata": {}, 147 | "source": [ 148 | "We can double-check that this is producing the desired result by comparing to the Fortran NUFFT implementation, using Python wrappers written by Dan Foreman-Mackey, available at [http://github.com/dfm/python-nufft/](http://github.com/dfm/python-nufft/):" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "collapsed": false, 154 | "input": [ 155 | "# Install nufft from http://github.com/dfm/python-nufft/\n", 156 | "from nufft import nufft1 as nufft_fortran\n", 157 | "\n", 158 | "x = 100 * np.random.random(1000)\n", 159 | "y = np.sin(x)\n", 160 | "\n", 161 | "Y1 = nudft(x, y, 1000)\n", 162 | "Y2 = nufft_fortran(x, y, 1000)\n", 163 | "\n", 164 | "np.allclose(Y1, Y2)" 165 | ], 166 | "language": "python", 167 | "metadata": {}, 168 | "outputs": [] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "The results match! A quick check shows that, as we might expect, the Fortran algorithm is orders of magnitude faster:" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "collapsed": false, 180 | "input": [ 181 | "%timeit nudft(x, y, 1000)\n", 182 | "%timeit nufft_fortran(x, y, 1000)" 183 | ], 184 | "language": "python", 185 | "metadata": {}, 186 | "outputs": [] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "On top of this, for $N$ points and $N$ frequencies, the Fortran NUFFT will scale as $O[N\\log N]$, while our simple implementation will scale as $O[N^2]$, making the difference even greater as $N$ increases! Let's see if we can do better." 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "metadata": {}, 198 | "source": [ 199 | "## NUFFT with Python\n", 200 | "\n", 201 | "Here we'll attempt a pure-Python version of the fast, FFT-based NUFFT.\n", 202 | "We'll follow the basics of the algorithm presented on the NUFFT page, using NumPy broadcasting tricks to push loops into the compiled layer of NumPy.\n", 203 | "For later convenience, we'll start by defining a utility to compute the grid parameters as detailed in the NUFFT paper." 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "collapsed": false, 209 | "input": [ 210 | "def _compute_grid_params(M, eps):\n", 211 | " # Choose Msp & tau from eps following Dutt & Rokhlin (1993)\n", 212 | " if eps <= 1E-33 or eps >= 1E-1:\n", 213 | " raise ValueError(\"eps = {0:.0e}; must satisfy \"\n", 214 | " \"1e-33 < eps < 1e-1.\".format(eps))\n", 215 | " ratio = 2 if eps > 1E-11 else 3\n", 216 | " Msp = int(-np.log(eps) / (np.pi * (ratio - 1) / (ratio - 0.5)) + 0.5)\n", 217 | " Mr = max(ratio * M, 2 * Msp)\n", 218 | " lambda_ = Msp / (ratio * (ratio - 0.5))\n", 219 | " tau = np.pi * lambda_ / M ** 2\n", 220 | " return Msp, Mr, tau\n", 221 | "\n", 222 | "\n", 223 | "def nufft_python(x, c, M, df=1.0, eps=1E-15, iflag=1):\n", 224 | " \"\"\"Fast Non-Uniform Fourier Transform with Python\"\"\"\n", 225 | " Msp, Mr, tau = _compute_grid_params(M, eps)\n", 226 | " N = len(x)\n", 227 | "\n", 228 | " # Construct the convolved grid\n", 229 | " ftau = np.zeros(Mr, dtype=c.dtype)\n", 230 | " Mr = ftau.shape[0]\n", 231 | " hx = 2 * np.pi / Mr\n", 232 | " mm = np.arange(-Msp, Msp)\n", 233 | " for i in range(N):\n", 234 | " xi = (x[i] * df) % (2 * np.pi)\n", 235 | " m = 1 + int(xi // hx)\n", 236 | " spread = np.exp(-0.25 * (xi - hx * (m + mm)) ** 2 / tau)\n", 237 | " ftau[(m + mm) % Mr] += c[i] * spread\n", 238 | "\n", 239 | " # Compute the FFT on the convolved grid\n", 240 | " if iflag < 0:\n", 241 | " Ftau = (1 / Mr) * np.fft.fft(ftau)\n", 242 | " else:\n", 243 | " Ftau = np.fft.ifft(ftau)\n", 244 | " Ftau = np.concatenate([Ftau[-(M//2):], Ftau[:M//2 + M % 2]])\n", 245 | "\n", 246 | " # Deconvolve the grid using convolution theorem\n", 247 | " k = nufftfreqs(M)\n", 248 | " return (1 / N) * np.sqrt(np.pi / tau) * np.exp(tau * k ** 2) * Ftau" 249 | ], 250 | "language": "python", 251 | "metadata": {}, 252 | "outputs": [] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "Let's compare this to the previous results.\n", 259 | "For convenience, we'll define a single routine which validates the results and times the execution:" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "collapsed": false, 265 | "input": [ 266 | "from time import time\n", 267 | "\n", 268 | "def test_nufft(nufft_func, M=1000, Mtime=100000):\n", 269 | " # Test vs the direct method\n", 270 | " print(30 * '-')\n", 271 | " name = {'nufft1':'nufft_fortran'}.get(nufft_func.__name__,\n", 272 | " nufft_func.__name__)\n", 273 | " print(\"testing {0}\".format(name))\n", 274 | " rng = np.random.RandomState(0)\n", 275 | " x = 100 * rng.rand(M + 1)\n", 276 | " y = np.sin(x)\n", 277 | " for df in [1, 2.0]:\n", 278 | " for iflag in [1, -1]:\n", 279 | " F1 = nudft(x, y, M, df=df, iflag=iflag)\n", 280 | " F2 = nufft_func(x, y, M, df=df, iflag=iflag)\n", 281 | " assert np.allclose(F1, F2)\n", 282 | " print(\"- Results match the DFT\")\n", 283 | " \n", 284 | " # Time the nufft function\n", 285 | " x = 100 * rng.rand(Mtime)\n", 286 | " y = np.sin(x)\n", 287 | " times = []\n", 288 | " for i in range(5):\n", 289 | " t0 = time()\n", 290 | " F = nufft_func(x, y, Mtime)\n", 291 | " t1 = time()\n", 292 | " times.append(t1 - t0)\n", 293 | " print(\"- Execution time (M={0}): {1:.2g} sec\".format(Mtime, np.median(times)))" 294 | ], 295 | "language": "python", 296 | "metadata": {}, 297 | "outputs": [] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "collapsed": false, 302 | "input": [ 303 | "test_nufft(nufft_python)\n", 304 | "test_nufft(nufft_fortran)" 305 | ], 306 | "language": "python", 307 | "metadata": {}, 308 | "outputs": [] 309 | }, 310 | { 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "The good news is that our Python implementation works; the bad news is that it remains several orders of magnitude slower than the Fortran result!\n", 315 | "\n", 316 | "Let's make it faster." 317 | ] 318 | }, 319 | { 320 | "cell_type": "markdown", 321 | "metadata": {}, 322 | "source": [ 323 | "### Making Code Faster: Line Profiling\n", 324 | "\n", 325 | "We know that our Python function is slow, but we'd like to determine *where* this speed bottleneck lies.\n", 326 | "One convenient way to do this is with the ``line_profiler`` utility, a Python/IPython addon which can be installed using\n", 327 | "```\n", 328 | "$ pip install line_profiler\n", 329 | "```\n", 330 | "Once it's installed, we can load the line profiler extension into the IPython notebook using the ``%load_ext`` magic function:" 331 | ] 332 | }, 333 | { 334 | "cell_type": "code", 335 | "collapsed": false, 336 | "input": [ 337 | "%load_ext line_profiler" 338 | ], 339 | "language": "python", 340 | "metadata": {}, 341 | "outputs": [] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "metadata": {}, 346 | "source": [ 347 | "With the line profiler loaded, the ``%lprun`` magic function is now available, which we can use to profile our function line-by-line.\n", 348 | "In order to display these results here, we'll save them to file and then use ``%cat`` to view the file:" 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "collapsed": false, 354 | "input": [ 355 | "%lprun -s -f nufft_python -T lp_results.txt nufft_python(x, y, 1000)\n", 356 | "%cat lp_results.txt" 357 | ], 358 | "language": "python", 359 | "metadata": {}, 360 | "outputs": [] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "metadata": {}, 365 | "source": [ 366 | "The output shows us where, line-by-line, the algorithm is spending the most time.\n", 367 | "We see that nearly 99% of the execution time is being spent in the single ``for`` loop at the center of our code.\n", 368 | "The loop is so expensive that even the FFT computation is just a trivial piece of the cost!\n", 369 | "This is actually pretty typical: due to dynamic typing, loops are generally very slow in Python.\n", 370 | "\n", 371 | "One of the surest strategies for speeding-up your code is to use broadcasting tricks in NumPy to remove these kinds of large loops: you can read one of my course lectures on the subject [here](http://nbviewer.ipython.org/url/www.astro.washington.edu/users/vanderplas/Astr599_2014/notebooks/11_EfficientNumpy.ipynb).\n", 372 | "We'll do this next." 373 | ] 374 | }, 375 | { 376 | "cell_type": "markdown", 377 | "metadata": {}, 378 | "source": [ 379 | "## NUFFT with NumPy Broadcasting\n", 380 | "\n", 381 | "Let's rewrite the above implementation and use broadcasting tricks to elliminate the loops.\n", 382 | "Because of the structure of this problem, the approach is a bit complicated here, but it turns out that we can take advantage here of the little-known ``at()`` method of NumPy's ufunc.\n", 383 | "Briefly,\n", 384 | "```python\n", 385 | ">>> np.add.at(x, i, y)\n", 386 | "```\n", 387 | "is similar to\n", 388 | "```python\n", 389 | ">>> x[i] += y\n", 390 | "```\n", 391 | "but works as desired even if the incides ``i`` have duplicate entries.\n", 392 | "\n", 393 | "Using this, we can adjust our implementation as follows:" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "collapsed": false, 399 | "input": [ 400 | "def nufft_numpy(x, y, M, df=1.0, iflag=1, eps=1E-15):\n", 401 | " \"\"\"Fast Non-Uniform Fourier Transform\"\"\"\n", 402 | " Msp, Mr, tau = _compute_grid_params(M, eps)\n", 403 | " N = len(x)\n", 404 | "\n", 405 | " # Construct the convolved grid ftau:\n", 406 | " # this replaces the loop used above\n", 407 | " ftau = np.zeros(Mr, dtype=y.dtype)\n", 408 | " hx = 2 * np.pi / Mr\n", 409 | " xmod = (x * df) % (2 * np.pi)\n", 410 | " m = 1 + (xmod // hx).astype(int)\n", 411 | " mm = np.arange(-Msp, Msp)\n", 412 | " mpmm = m + mm[:, np.newaxis]\n", 413 | " spread = y * np.exp(-0.25 * (xmod - hx * mpmm) ** 2 / tau)\n", 414 | " np.add.at(ftau, mpmm % Mr, spread)\n", 415 | "\n", 416 | " # Compute the FFT on the convolved grid\n", 417 | " if iflag < 0:\n", 418 | " Ftau = (1 / Mr) * np.fft.fft(ftau)\n", 419 | " else:\n", 420 | " Ftau = np.fft.ifft(ftau)\n", 421 | " Ftau = np.concatenate([Ftau[-(M//2):], Ftau[:M//2 + M % 2]])\n", 422 | "\n", 423 | " # Deconvolve the grid using convolution theorem\n", 424 | " k = nufftfreqs(M)\n", 425 | " return (1 / N) * np.sqrt(np.pi / tau) * np.exp(tau * k ** 2) * Ftau" 426 | ], 427 | "language": "python", 428 | "metadata": {}, 429 | "outputs": [] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "metadata": {}, 434 | "source": [ 435 | "Let's test it:" 436 | ] 437 | }, 438 | { 439 | "cell_type": "code", 440 | "collapsed": false, 441 | "input": [ 442 | "test_nufft(nufft_numpy)\n", 443 | "test_nufft(nufft_python)\n", 444 | "test_nufft(nufft_fortran)" 445 | ], 446 | "language": "python", 447 | "metadata": {}, 448 | "outputs": [] 449 | }, 450 | { 451 | "cell_type": "markdown", 452 | "metadata": {}, 453 | "source": [ 454 | "It worked! We gained around a factor of 4 speedup in replacing the Python loop with the ``np.add.at()`` call.\n", 455 | "Still, though, we're sitting at about a factor of 10 slower than the Fortran version.\n", 456 | "The problem is that the ``np.add.at()`` call here requires construction of some very large and costly temporary arrays.\n", 457 | "If we want a faster execution time, we need to further optimize that main loop, and we can't do this with NumPy alone." 458 | ] 459 | }, 460 | { 461 | "cell_type": "markdown", 462 | "metadata": {}, 463 | "source": [ 464 | "## Optimization with Numba\n", 465 | "\n", 466 | "When NumPy broadcasting tricks aren't enough, there are a few options: you can write Fortran or C code directly, you can use [Cython](http://cython.org), [Weave](http://docs.scipy.org/doc/scipy/reference/tutorial/weave.html), or other tools as a bridge to include compiled snippets in your script, or you can use a tool like [Numba](http://cython.org) to speed-up your loops without ever leaving Python.\n", 467 | "\n", 468 | "Numba is a slick tool which runs Python functions through an LLVM just-in-time (JIT) compiler, leading to orders-of-magnitude faster code for certain operations.\n", 469 | "In this case, we need to optimize what amounts to a nested for-loop, so Numba fits the bill perfectly.\n", 470 | "For clarity, we'll pull-out the grid construction code that we want to optimize, and write it as follows:" 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "collapsed": false, 476 | "input": [ 477 | "import numba\n", 478 | "\n", 479 | "# nopython=True means an error will be raised\n", 480 | "# if fast compilation is not possible.\n", 481 | "@numba.jit(nopython=True)\n", 482 | "def build_grid(x, c, tau, Msp, ftau):\n", 483 | " Mr = ftau.shape[0]\n", 484 | " hx = 2 * np.pi / Mr\n", 485 | " for i in range(x.shape[0]):\n", 486 | " xi = x[i] % (2 * np.pi)\n", 487 | " m = 1 + int(xi // hx)\n", 488 | " for mm in range(-Msp, Msp):\n", 489 | " ftau[(m + mm) % Mr] += c[i] * np.exp(-0.25 * (xi - hx * (m + mm)) ** 2 / tau)\n", 490 | " return ftau\n", 491 | "\n", 492 | "\n", 493 | "def nufft_numba(x, c, M, df=1.0, eps=1E-15, iflag=1):\n", 494 | " \"\"\"Fast Non-Uniform Fourier Transform with Numba\"\"\"\n", 495 | " Msp, Mr, tau = _compute_grid_params(M, eps)\n", 496 | " N = len(x)\n", 497 | "\n", 498 | " # Construct the convolved grid\n", 499 | " ftau = build_grid(x * df, c, tau, Msp,\n", 500 | " np.zeros(Mr, dtype=c.dtype))\n", 501 | "\n", 502 | " # Compute the FFT on the convolved grid\n", 503 | " if iflag < 0:\n", 504 | " Ftau = (1 / Mr) * np.fft.fft(ftau)\n", 505 | " else:\n", 506 | " Ftau = np.fft.ifft(ftau)\n", 507 | " Ftau = np.concatenate([Ftau[-(M//2):], Ftau[:M//2 + M % 2]])\n", 508 | "\n", 509 | " # Deconvolve the grid using convolution theorem\n", 510 | " k = nufftfreqs(M)\n", 511 | " return (1 / N) * np.sqrt(np.pi / tau) * np.exp(tau * k ** 2) * Ftau" 512 | ], 513 | "language": "python", 514 | "metadata": {}, 515 | "outputs": [] 516 | }, 517 | { 518 | "cell_type": "markdown", 519 | "metadata": {}, 520 | "source": [ 521 | "Let's test this now:" 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "collapsed": false, 527 | "input": [ 528 | "test_nufft(nufft_numba)\n", 529 | "test_nufft(nufft_fortran)" 530 | ], 531 | "language": "python", 532 | "metadata": {}, 533 | "outputs": [] 534 | }, 535 | { 536 | "cell_type": "markdown", 537 | "metadata": {}, 538 | "source": [ 539 | "Much better! We're now within about a factor of 3 of the Fortran speed, and we're still writing pure Python!\n", 540 | "\n", 541 | "Having plucked all the low-hanging fruit, any further optimization will now be very low-level: that is, thinking about things like reduction of the number of ``exp()`` evaluations through application of mathematical identities.\n", 542 | "This type of careful logic is one reason the Fortran implementation is so fast, and many of these low-level strategies are discussed in the NUFFT paper linked above.\n", 543 | "\n", 544 | "To gain some more speed, we can follow their advice and optimize the expressions at this level by precomputing expensive expressions and recombining these expressions later:\n", 545 | "This makes the algorithm a bit more obfuscated, but it does lead to some faster execution.\n", 546 | "Here is an example of this:" 547 | ] 548 | }, 549 | { 550 | "cell_type": "code", 551 | "collapsed": false, 552 | "input": [ 553 | "import numba\n", 554 | "\n", 555 | "@numba.jit(nopython=True)\n", 556 | "def build_grid_fast(x, c, tau, Msp, ftau, E3):\n", 557 | " Mr = ftau.shape[0]\n", 558 | " hx = 2 * np.pi / Mr\n", 559 | " \n", 560 | " # precompute some exponents\n", 561 | " for j in range(Msp + 1):\n", 562 | " E3[j] = np.exp(-(np.pi * j / Mr) ** 2 / tau)\n", 563 | " \n", 564 | " # spread values onto ftau\n", 565 | " for i in range(x.shape[0]):\n", 566 | " xi = x[i] % (2 * np.pi)\n", 567 | " m = 1 + int(xi // hx)\n", 568 | " xi = (xi - hx * m)\n", 569 | " E1 = np.exp(-0.25 * xi ** 2 / tau)\n", 570 | " E2 = np.exp((xi * np.pi) / (Mr * tau))\n", 571 | " E2mm = 1\n", 572 | " for mm in range(Msp):\n", 573 | " ftau[(m + mm) % Mr] += c[i] * E1 * E2mm * E3[mm]\n", 574 | " E2mm *= E2\n", 575 | " ftau[(m - mm - 1) % Mr] += c[i] * E1 / E2mm * E3[mm + 1]\n", 576 | " return ftau\n", 577 | "\n", 578 | "\n", 579 | "def nufft_numba_fast(x, c, M, df=1.0, eps=1E-15, iflag=1):\n", 580 | " \"\"\"Fast Non-Uniform Fourier Transform with Numba\"\"\"\n", 581 | " Msp, Mr, tau = _compute_grid_params(M, eps)\n", 582 | " N = len(x)\n", 583 | "\n", 584 | " # Construct the convolved grid\n", 585 | " ftau = build_grid_fast(x * df, c, tau, Msp,\n", 586 | " np.zeros(Mr, dtype=c.dtype),\n", 587 | " np.zeros(Msp + 1, dtype=x.dtype))\n", 588 | "\n", 589 | " # Compute the FFT on the convolved grid\n", 590 | " if iflag < 0:\n", 591 | " Ftau = (1 / Mr) * np.fft.fft(ftau)\n", 592 | " else:\n", 593 | " Ftau = np.fft.ifft(ftau)\n", 594 | " Ftau = np.concatenate([Ftau[-(M//2):], Ftau[:M//2 + M % 2]])\n", 595 | "\n", 596 | " # Deconvolve the grid using convolution theorem\n", 597 | " k = nufftfreqs(M)\n", 598 | " return (1 / N) * np.sqrt(np.pi / tau) * np.exp(tau * k ** 2) * Ftau" 599 | ], 600 | "language": "python", 601 | "metadata": {}, 602 | "outputs": [] 603 | }, 604 | { 605 | "cell_type": "markdown", 606 | "metadata": {}, 607 | "source": [ 608 | "Let's test the result:" 609 | ] 610 | }, 611 | { 612 | "cell_type": "code", 613 | "collapsed": false, 614 | "input": [ 615 | "test_nufft(nufft_numba_fast)\n", 616 | "test_nufft(nufft_fortran)" 617 | ], 618 | "language": "python", 619 | "metadata": {}, 620 | "outputs": [] 621 | }, 622 | { 623 | "cell_type": "markdown", 624 | "metadata": {}, 625 | "source": [ 626 | "This is looking good! With a bit of effort we are now within about 25% of the Fortran speed, and we retain all the advantages of having pure Python code!" 627 | ] 628 | }, 629 | { 630 | "cell_type": "markdown", 631 | "metadata": {}, 632 | "source": [ 633 | "## Final Timing Comparison\n", 634 | "\n", 635 | "For good measure, let's take a look at the scaling with $M$ for all the fast algorithms we created.\n", 636 | "We'll compute the times for a range of input sizes for each algorithm.\n", 637 | "Be aware that the following code will take several minutes to run!" 638 | ] 639 | }, 640 | { 641 | "cell_type": "code", 642 | "collapsed": false, 643 | "input": [ 644 | "%matplotlib inline\n", 645 | "import matplotlib.pyplot as plt\n", 646 | "# use seaborn for nice default plot settings\n", 647 | "import seaborn; seaborn.set()" 648 | ], 649 | "language": "python", 650 | "metadata": {}, 651 | "outputs": [] 652 | }, 653 | { 654 | "cell_type": "code", 655 | "collapsed": false, 656 | "input": [ 657 | "Mrange = (2 ** np.arange(3, 18)).astype(int)\n", 658 | "\n", 659 | "t_python = []\n", 660 | "t_numpy = []\n", 661 | "t_numba = []\n", 662 | "t_numba_fast = []\n", 663 | "t_fortran = []\n", 664 | "\n", 665 | "for M in Mrange:\n", 666 | " x = 100 * np.random.random(M)\n", 667 | " c = np.sin(x)\n", 668 | " \n", 669 | " t1 = %timeit -oq nufft_python(x, c, M)\n", 670 | " t2 = %timeit -oq nufft_numpy(x, c, M)\n", 671 | " t3 = %timeit -oq nufft_numba(x, c, M)\n", 672 | " t4 = %timeit -oq nufft_numba_fast(x, c, M)\n", 673 | " t5 = %timeit -oq nufft_fortran(x, c, M)\n", 674 | " \n", 675 | " t_python.append(t1.best)\n", 676 | " t_numpy.append(t2.best)\n", 677 | " t_numba.append(t3.best)\n", 678 | " t_numba_fast.append(t4.best)\n", 679 | " t_fortran.append(t5.best)" 680 | ], 681 | "language": "python", 682 | "metadata": {}, 683 | "outputs": [] 684 | }, 685 | { 686 | "cell_type": "code", 687 | "collapsed": false, 688 | "input": [ 689 | "plt.loglog(Mrange, t_python, label='python')\n", 690 | "plt.loglog(Mrange, t_numpy, label='numpy')\n", 691 | "plt.loglog(Mrange, t_numba, label='numba #1')\n", 692 | "plt.loglog(Mrange, t_numba_fast, label='numba #2')\n", 693 | "plt.loglog(Mrange, t_fortran, label='fortran')\n", 694 | "plt.legend(loc='upper left')\n", 695 | "plt.xlabel('Number of Elements')\n", 696 | "plt.ylabel('Execution Time (s)');" 697 | ], 698 | "language": "python", 699 | "metadata": {}, 700 | "outputs": [] 701 | }, 702 | { 703 | "cell_type": "markdown", 704 | "metadata": {}, 705 | "source": [ 706 | "As we see, all the algorithms scale as $\\sim O[N\\log N]$ in the large $N$ limit, albeit with very different constants of proportionality.\n", 707 | "Our final optimized Numba implementation nearly matches the Fortran version as $N$ grows large, and because it is written in pure Python, it retains all the advantages of pure Python code listed above.\n", 708 | "For that benefit, I think the cost of a ~25% slow-down is well worth it!" 709 | ] 710 | }, 711 | { 712 | "cell_type": "markdown", 713 | "metadata": {}, 714 | "source": [ 715 | "## Conclusion\n", 716 | "\n", 717 | "I hope you've enjoyed this exploration of how to write fast numerical code in pure Python.\n", 718 | "As you think about writing efficient implementations of useful algorithms, I invite you to consider the points I listed above: in particular, how difficult will it be for your users to install, read, modify, and contribute to your code?\n", 719 | "In the long run, this may be much more important than shaving a few milliseconds off the execution time.\n", 720 | "Writing a fast implementation of a useful algorithm is an excellent and useful pursuit, but we should be careful to not forget the costs that come along with such optimization.\n", 721 | "\n", 722 | "If you're interested in using the pure-Python NUFFT implementation, I've adapted much of the above code in a repository at [http://github.com/jakevdp/nufftpy/](http://github.com/jakevdp/nufftpy/).\n", 723 | "It contains a packaged and unit-tested version of some of the above code, as well as a (hopefully) growing compendium of related routines." 724 | ] 725 | }, 726 | { 727 | "cell_type": "markdown", 728 | "metadata": {}, 729 | "source": [ 730 | "\n", 731 | "This post was written entirely in the IPython notebook. You can\n", 732 | "[download](http://jakevdp.github.io/downloads/notebooks/NUFFT.ipynb)\n", 733 | "this notebook, or see a static view\n", 734 | "[here](http://nbviewer.ipython.org/url/jakevdp.github.io/downloads/notebooks/NUFFT.ipynb).\n", 735 | "" 736 | ] 737 | } 738 | ], 739 | "metadata": {} 740 | } 741 | ] 742 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NUFFT in Python 2 | 3 | This repository holds experiments with pure-python implementations of the NUFFT and related algorithms. 4 | For more information see the [NUFFT page](http://www.cims.nyu.edu/cmcl/nufft/nufft.html), and also see the [python-nufft](https://github.com/dfm/python-nufft) package which provides simple Python wrappers for the nufft Fortran source. 5 | 6 | For a discussion of the Python version of this algorithm, see my [blog post](https://jakevdp.github.io/blog/2015/02/24/optimizing-python-with-numpy-and-numba/) on the subject. -------------------------------------------------------------------------------- /nufftpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .nufft import nufft1, nufftfreqs 2 | 3 | __version__ = '0.1-git' 4 | -------------------------------------------------------------------------------- /nufftpy/_numba_tools.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import numpy as np 3 | import numba 4 | 5 | 6 | @numba.jit(nopython=True) 7 | def slow_grid(x, c, tau, Msp, ftau): 8 | Mr = ftau.shape[0] 9 | hx = 2 * np.pi / Mr 10 | for i in range(x.shape[0]): 11 | xi = x[i] % (2 * np.pi) 12 | m = 1 + int(xi // hx) 13 | for mm in range(-Msp, Msp): 14 | spread = np.exp(-0.25 * (xi - hx * (m + mm)) ** 2 / tau) 15 | ftau[(m + mm) % Mr] += c[i] * spread 16 | 17 | 18 | @numba.jit(nopython=True) 19 | def fast_grid(x, c, tau, Msp, ftau, E3): 20 | Mr = ftau.shape[0] 21 | hx = 2 * np.pi / Mr 22 | 23 | # precompute some exponents 24 | for j in range(Msp + 1): 25 | E3[j] = np.exp(-(np.pi * j / Mr) ** 2 / tau) 26 | 27 | # spread values onto ftau 28 | for i in range(x.shape[0]): 29 | xi = x[i] % (2 * np.pi) 30 | m = 1 + int(xi // hx) 31 | xi = (xi - hx * m) 32 | E1 = np.exp(-0.25 * xi ** 2 / tau) 33 | E2 = np.exp((xi * np.pi) / (Mr * tau)) 34 | E2mm = 1 35 | for mm in range(Msp): 36 | ftau[(m + mm) % Mr] += c[i] * E1 * E2mm * E3[mm] 37 | E2mm *= E2 38 | ftau[(m - mm - 1) % Mr] += c[i] * E1 / E2mm * E3[mm + 1] 39 | 40 | 41 | def _gaussian_grid_numba_1D(x, c, Mr, Msp, tau, fast_gridding): 42 | """Compute the 1D Gaussian gridding with numba""" 43 | ftau = np.zeros(Mr, dtype=c.dtype) 44 | if fast_gridding: 45 | E3 = np.zeros(Msp + 1, dtype=x.dtype) 46 | fast_grid(x, c, tau, Msp, ftau, E3) 47 | else: 48 | slow_grid(x, c, tau, Msp, ftau) 49 | return ftau 50 | -------------------------------------------------------------------------------- /nufftpy/nufft.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | import warnings 3 | import numpy as np 4 | 5 | try: 6 | import numba 7 | except ImportError: 8 | numba = None 9 | else: 10 | from ._numba_tools import _gaussian_grid_numba_1D 11 | 12 | 13 | def nufftfreqs(M, df=1): 14 | """Compute the frequency range used in nufft for M frequency bins""" 15 | return df * np.arange(-(M // 2), M - (M // 2)) 16 | 17 | 18 | def _check_inputs(x, c, df): 19 | x = df * np.asarray(x) 20 | c = np.asarray(c) 21 | if x.ndim != 1: 22 | raise ValueError("Expected one-dimensional input arrays") 23 | if x.shape != c.shape: 24 | raise ValueError("Array shapes must match") 25 | return x, c 26 | 27 | 28 | def _compute_grid_params(M, eps): 29 | if eps <= 1E-33 or eps >= 1E-1: 30 | raise ValueError("eps = {0:.0e}; must satisfy " 31 | "1e-33 < eps < 1e-1.".format(eps)) 32 | 33 | # Choose Msp & tau from eps following Dutt & Rokhlin (1993) 34 | ratio = 2 if eps > 1E-11 else 3 35 | Msp = int(-np.log(eps) / (np.pi * (ratio - 1) / (ratio - 0.5)) + 0.5) 36 | Mr = max(ratio * M, 2 * Msp) 37 | lambda_ = Msp / (ratio * (ratio - 0.5)) 38 | tau = np.pi * lambda_ / M ** 2 39 | return Mr, Msp, tau 40 | 41 | 42 | def _gaussian_grid_1D(x, c, Mr, Msp, tau, fast_gridding): 43 | """Compute the 1D gaussian gridding with Numpy""" 44 | N = len(x) 45 | ftau = np.zeros(Mr, dtype=c.dtype) 46 | hx = 2 * np.pi / Mr 47 | xmod = x % (2 * np.pi) 48 | 49 | m = 1 + (xmod // hx).astype(int) 50 | msp = np.arange(-Msp, Msp)[:, np.newaxis] 51 | mm = m + msp 52 | 53 | if fast_gridding: 54 | # Greengard & Lee (2004) approach 55 | E1 = np.exp(-0.25 * (xmod - hx * m) ** 2 / tau) 56 | 57 | # The following lines basically compute this: 58 | # E2 = np.exp(msp * (xmod - hx * m) * np.pi / (Mr * tau)) 59 | E2 = np.empty((2 * Msp, N), dtype=xmod.dtype) 60 | E2[Msp] = 1 61 | E2[Msp + 1:] = np.exp((xmod - hx * m) * np.pi / (Mr * tau)) 62 | E2[Msp + 1:].cumprod(0, out=E2[Msp + 1:]) 63 | E2[Msp - 1::-1] = 1. / (E2[Msp + 1] * E2[Msp:]) 64 | 65 | E3 = np.exp(-(np.pi * msp / Mr) ** 2 / tau) 66 | spread = (c * E1) * E2 * E3 67 | else: 68 | spread = c * np.exp(-0.25 * (xmod - hx * mm) ** 2 / tau) 69 | np.add.at(ftau, mm % Mr, spread) 70 | 71 | return ftau 72 | 73 | 74 | def nufft1(x, c, M, df=1.0, eps=1E-15, iflag=1, 75 | direct=False, fast_gridding=True, use_numba=True): 76 | """Fast Non-Uniform Fourier Transform (Type 1: uniform frequency grid) 77 | 78 | Compute the non-uniform FFT of one-dimensional points x with complex 79 | values c. Result is computed at frequencies (df * m) 80 | for integer m in the range -M/2 < m < M/2. 81 | 82 | Parameters 83 | ---------- 84 | x, c : array_like 85 | real locations x and complex values c of the points to be transformed. 86 | M, df : int & float 87 | Parameters specifying the desired frequency grid. Transform will be 88 | computed at frequencies df * (-(M//2) + arange(M)) 89 | eps : float 90 | The desired approximate error for the FFT result. Must be in range 91 | 1E-33 < eps < 1E-1, though be aware that the errors are only well 92 | calibrated near the range 1E-12 ~ 1E-6. eps is not referenced if 93 | direct = True. 94 | iflag : float 95 | if iflag<0, compute the transform with a negative exponent. 96 | if iflag>=0, compute the transform with a positive exponent. 97 | direct : bool (default = False) 98 | If True, then use the slower (but more straightforward) 99 | direct Fourier transform to compute the result. 100 | fast_gridding : bool (default = True) 101 | If True, use the fast Gaussian grid algorithm of Greengard & Lee (2004) 102 | Otherwise, use a more naive gridding approach 103 | use_numba : bool (default = True) 104 | If True, use numba to compute the result. If False or if numba is not 105 | installed, default to the numpy version, which is ~5x slower. 106 | 107 | Returns 108 | ------- 109 | Fk : ndarray 110 | The complex discrete Fourier transform 111 | 112 | See Also 113 | -------- 114 | nufftfreqs : compute the frequencies of the nufft results 115 | """ 116 | x, c = _check_inputs(x, c, df) 117 | M = int(M) 118 | N = len(x) 119 | k = nufftfreqs(M) 120 | 121 | if direct: 122 | # Direct Fourier Transform: this is easy (but slow) 123 | sign = -1 if iflag < 0 else 1 124 | return (1 / N) * np.dot(c, np.exp(sign * 1j * k * x[:, None])) 125 | else: 126 | # FFT version: a bit more involved 127 | Mr, Msp, tau = _compute_grid_params(M, eps) 128 | 129 | # Construct the convolved grid 130 | if use_numba and (numba is None): 131 | warnings.warn('numba is not installed. Using slower version.') 132 | use_numba = False 133 | if use_numba: 134 | ftau = _gaussian_grid_numba_1D(x, c, Mr, Msp, tau, fast_gridding) 135 | else: 136 | ftau = _gaussian_grid_1D(x, c, Mr, Msp, tau, fast_gridding) 137 | 138 | # Compute the FFT on the convolved grid 139 | if iflag < 0: 140 | Ftau = (1 / Mr) * np.fft.fft(ftau) 141 | else: 142 | Ftau = np.fft.ifft(ftau) 143 | Ftau = np.concatenate([Ftau[-(M//2):], Ftau[:M//2 + M % 2]]) 144 | 145 | # Deconvolve the grid using convolution theorem 146 | return (1 / N) * np.sqrt(np.pi / tau) * np.exp(tau * k ** 2) * Ftau 147 | -------------------------------------------------------------------------------- /nufftpy/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakevdp/nufftpy/897ee804ec04b22cc3332a78e1537e555b62455f/nufftpy/tests/__init__.py -------------------------------------------------------------------------------- /nufftpy/tests/test_nufft1d.py: -------------------------------------------------------------------------------- 1 | from .. import nufft1 2 | 3 | import numpy as np 4 | from numpy.testing import assert_allclose 5 | 6 | from nose import SkipTest 7 | 8 | 9 | try: 10 | import numba 11 | except ImportError: 12 | numba = None 13 | 14 | 15 | def test_nufft1_vs_direct_1D(): 16 | rng = np.random.RandomState(0) 17 | x = 100 * rng.rand(100) 18 | c = np.exp(1j * x) 19 | 20 | def check_results(df, iflag, M, eps, use_numba, fast_gridding): 21 | if use_numba and numba is None: 22 | raise SkipTest("numba is not installed") 23 | 24 | # Test complex inputs 25 | dft = nufft1(x, c, M, iflag=iflag, direct=True) 26 | fft = nufft1(x, c, M, iflag=iflag, eps=eps, 27 | use_numba=use_numba, fast_gridding=fast_gridding) 28 | assert_allclose(dft, fft, rtol=eps ** 0.95) 29 | 30 | # Test real inputs 31 | dft = nufft1(x, c.real, M, iflag=iflag, direct=True) 32 | fft = nufft1(x, c.real, M, iflag=iflag, eps=eps, 33 | use_numba=use_numba, fast_gridding=fast_gridding) 34 | assert_allclose(dft, fft, rtol=eps ** 0.95) 35 | 36 | for df in [0.5, 1.0, 2.0]: 37 | for iflag in [1, -1]: 38 | for M in [51, 100, 111]: 39 | for eps in [1E-8, 1E-12]: 40 | for use_numba in [True, False]: 41 | for fast_gridding in [True, False]: 42 | yield (check_results, df, iflag, M, eps, 43 | use_numba, fast_gridding) 44 | 45 | 46 | def test_nufft1_vs_fortran_1D(): 47 | """Test against the Fortran implementation""" 48 | try: 49 | from nufft import nufft1 as nufft1_fortran 50 | except ImportError: 51 | raise SkipTest("python-nufft package is not installed: " 52 | "see http://github.com/dfm/python-nufft") 53 | rng = np.random.RandomState(0) 54 | x = 100 * rng.rand(100) 55 | c = np.exp(1j * x) 56 | 57 | def check_results(df, iflag, M, eps, use_numba): 58 | if use_numba and numba is None: 59 | raise SkipTest("numba is not installed") 60 | 61 | # test complex inputs 62 | fft1 = nufft1(x, c, M, iflag=iflag, eps=eps, use_numba=use_numba) 63 | fft2 = nufft1_fortran(x, c, M, iflag=iflag, eps=eps) 64 | assert_allclose(fft1, fft2) 65 | 66 | # test real inputs 67 | fft1 = nufft1(x, c.real, M, iflag=iflag, eps=eps, use_numba=use_numba) 68 | fft2 = nufft1_fortran(x, c.real, M, iflag=iflag, eps=eps) 69 | assert_allclose(fft1, fft2) 70 | 71 | for df in [0.5, 1.0, 2.0]: 72 | for iflag in [1, -1]: 73 | for M in [51, 100, 111]: 74 | for eps in [1E-8, 1E-12]: 75 | for use_numba in [True, False]: 76 | yield check_results, df, iflag, M, eps, use_numba 77 | 78 | 79 | def test_nufft1_fastgridding_1D(): 80 | rng = np.random.RandomState(0) 81 | x = 100 * rng.rand(100) 82 | c = np.exp(1j * x) 83 | 84 | def check_results(df, iflag, M, eps, use_numba): 85 | if use_numba and numba is None: 86 | raise SkipTest("numba is not installed") 87 | 88 | # test complex inputs 89 | fft1 = nufft1(x, c, M, iflag=iflag, eps=eps, 90 | use_numba=use_numba, fast_gridding=True) 91 | fft2 = nufft1(x, c, M, iflag=iflag, eps=eps, 92 | use_numba=use_numba, fast_gridding=False) 93 | assert_allclose(fft1, fft2) 94 | 95 | # test real inputs 96 | fft1 = nufft1(x, c.real, M, iflag=iflag, eps=eps, 97 | use_numba=use_numba, fast_gridding=True) 98 | fft2 = nufft1(x, c.real, M, iflag=iflag, eps=eps, 99 | use_numba=use_numba, fast_gridding=False) 100 | assert_allclose(fft1, fft2) 101 | 102 | for df in [0.5, 1.0, 2.0]: 103 | for iflag in [1, -1]: 104 | for M in [51, 100, 111]: 105 | for eps in [1E-8, 1E-12]: 106 | for use_numba in [True, False]: 107 | yield check_results, df, iflag, M, eps, use_numba 108 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | DESCRIPTION = "General tools for Astronomical Time Series in Python" 4 | LONG_DESCRIPTION = """ 5 | nufftpy: Non-Uniform FFT in Python 6 | ================================== 7 | 8 | This is a pure python implementation of the NUFFT. 9 | For more information, visit http://github.com/jakevdp/nufftpy 10 | """ 11 | NAME = "nufftpy" 12 | AUTHOR = "Jake VanderPlas" 13 | AUTHOR_EMAIL = "jakevdp@uw.edu" 14 | MAINTAINER = "Jake VanderPlas" 15 | MAINTAINER_EMAIL = "jakevdp@uw.edu" 16 | URL = 'http://github.com/jakevdp/nufftpy' 17 | DOWNLOAD_URL = 'http://github.com/jakevdp/nufftpy' 18 | LICENSE = 'BSD 3-clause' 19 | 20 | import nufftpy 21 | VERSION = nufftpy.__version__ 22 | 23 | setup(name=NAME, 24 | version=VERSION, 25 | description=DESCRIPTION, 26 | long_description=LONG_DESCRIPTION, 27 | author=AUTHOR, 28 | author_email=AUTHOR_EMAIL, 29 | maintainer=MAINTAINER, 30 | maintainer_email=MAINTAINER_EMAIL, 31 | url=URL, 32 | download_url=DOWNLOAD_URL, 33 | license=LICENSE, 34 | packages=['nufftpy', 35 | 'nufftpy.tests', 36 | ], 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta', 39 | 'Environment :: Console', 40 | 'Intended Audience :: Science/Research', 41 | 'License :: OSI Approved :: BSD License', 42 | 'Natural Language :: English', 43 | 'Programming Language :: Python :: 2.6', 44 | 'Programming Language :: Python :: 2.7', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4'], 47 | ) 48 | --------------------------------------------------------------------------------