{"action":"created","comment":{"url":"https://api.github.com/repos/mperrin/poppy/pulls/comments/188770743","pull_request_review_id":120817765,"id":188770743,"diff_hunk":"@@ -64,16 +88,220 @@ def _exp(x):\n         return np.exp(x)\n \n def _fftshift(x):\n+    \"\"\" FFT shifts of array contents, using CUDA if available.\n+    Otherwise defaults to numpy.\n+\n+    Note - TODO write an OpenCL version\n+\n+    See also ifftshift\n+    \"\"\"\n \n     N=x.shape[0]\n-    if (_USE_CUDA) & (N==x.shape[1]):\n+    # the CUDA fftshift is set up to work on blocks of 32, so\n+    # N must be a multiple of 32. We check this rapidly using a bit mask:\n+    #    (x & 31)==0  is a ~20x faster equivalent of (np.mod(x,32)==0)\n+    if (_USE_CUDA) & (N==x.shape[1]) & ((N & 31)==0):\n         blockdim = (32, 32) # threads per block\n         numBlocks = (int(N/blockdim[0]),int(N/blockdim[1]))\n         cufftShift_2D_kernel[numBlocks, blockdim](x.ravel(),N)\n         return x\n     else:\n         return np.fft.fftshift(x)\n \n+def _ifftshift(x):\n+    \"\"\" Inverse FFT shifts of array contents, using CUDA if available.\n+    Otherwise defaults to numpy.\n+    Note, ifftshift and fftshift are identical for even-length x,\n+    the functions differ by one sample for odd-length x. This function\n+    implictly assumes that if using CUDA, the array size must be even,\n+    so we can use the same algorithm as fftshift.\n+\n+    Note - TODO write an OpenCL version\n+\n+    See also fftshift\n+    \"\"\"\n+\n+    N=x.shape[0]\n+    # the CUDA fftshift is set up to work on blocks of 32, so\n+    # N must be a multiple of 32. We check this rapidly using a bit mask:\n+    #   not (x & 31)  is a ~20x faster equivalent of (np.mod(x,32)==0)\n+    if (_USE_CUDA) & (N==x.shape[1]) & ((N & 31)==0):\n+        blockdim = (32, 32) # threads per block\n+        numBlocks = (int(N/blockdim[0]),int(N/blockdim[1]))\n+        cufftShift_2D_kernel[numBlocks, blockdim](x.ravel(),N)\n+        return x\n+    else:\n+        return np.fft.ifftshift(x)\n+\n+\n+\n+\n+def fft_2d(wavefront, forward=True, normalization=None, fftshift=True):\n+    \"\"\" main entry point for FFTs, used in Wavefront._propagate_fft and\n+    elsewhere. This will invoke one of the following, depending on availability:\n+        - CUDA on NVidia GPU\n+        - OpenCL on AMD GPU\n+        - FFTW on CPU\n+        - numpy on CPU\n+\n+    This function handles ONLY the core numerics itself, as fast as possible,\n+    (and some minor related logging) .\n+    All the interaction with object state for Wavefront arrays should happen elsewhere.\n+\n+    TODO: this should execute an IN PLACE FFT, so we don't have to pass around arrays to return\n+    anything.\n+\n+    Parameters\n+    -----------\n+    forward : bool\n+        set to True for forward FFT, False for inverse fft\n+    normalization : float, optional\n+        Normalization factor. Defaults to 1./wavefront.shape[0] for forward,\n+        and wavefront.shape[0] for inverse. Use this only if you need a non-default\n+        behavior.\n+    fftshift : bool\n+        apply FFT shift after forwards FFT or before inverse FFT?\n+\n+    \"\"\"\n+    # To use a fast FFT, it must both be enabled and the library itself has to be present\n+    _USE_CUDA = (conf.use_cuda and _CUDA_AVAILABLE)\n+    _USE_OPENCL = (conf.use_opencl and _OPENCL_AVAILABLE)\n+    _USE_FFTW = (conf.use_fftw and _FFTW_AVAILABLE)\n+\n+    # OpenCL cfFFT only can FFT certain array sizes.\n+    # This check is more stringent that necessary - opencl can handle powers of a few small integers\n+    # but this simple version helps during development\n+    if _USE_OPENCL and not ispowerof2(wavefront.shape[0]):\n+        _log.debug((\"Wavefront size {} not supported by OpenCL, therefore disabling \"+\n+            \"USE_OPENCL for this calculation.\").format(wavefront.shape))\n+        _USE_OPENCL = False\n+\n+    # This annoyingly complicated if/elif is just for the debug print statement\n+    if _USE_CUDA:\n+        method = 'pyculib (CUDA GPU)'\n+    elif _USE_OPENCL:\n+        method = 'pyopencl (OpenCL GPU)'\n+    elif _USE_FFTW:\n+        method = 'pyfftw'\n+    else:\n+        method = 'numpy'\n+    _log.debug(\"using {2} FFT of {0} array, FFT_direction={1}\".format(str(wavefront.shape), 'forward' if forward else 'backward', method))\n+\n+    if (not forward) and fftshift: #inverse shift before backwards FFTs\n+        wavefront = _ifftshift(wavefront)\n+\n+    if _USE_CUDA:\n+        if normalization is None:\n+            normalization = 1./wavefront.shape[0]  # regardless of direction, for CUDA\n+\n+        # We need a CUDA FFT plan for each size and shape of FFT.\n+        cufftplan = pyculib.fft.FFTPlan( wavefront.shape, wavefront.dtype, wavefront.dtype)\n+\n+        # The plans could in principle be cached for reuse\n+        # FIXME - there appears to be a memory leak with doing it this way. Something never gets released\n+        # when we try to cache this, eventually leading to GPU runtime failure from inability to malloc\n+        # new array buffers. . The plan creation takes < 0.5 ms so it's perhaps acceptable to not cache \n+        # the results. At least for now this is worth it for the much better reliability\n+        #try:\n+            #params = (wavefront.shape, wavefront.dtype, wavefront.dtype)\n+            #cufftplan = _CUDA_PLANS[params]\n+            #print(\"cache hit, {},  {}\".format(params, cufftplan))\n+        #except KeyError:\n+            #cufftplan = pyculib.fft.FFTPlan(*params)\n+            #_CUDA_PLANS[params] = cufftplan\n+            #print(\"cache miss, {},  {}\".format(params, cufftplan))\n+\n+        # perform FFT on GPU, and return results in place to same array.\n+        if forward:\n+            cufftplan.forward(wavefront, out=wavefront)\n+        else:\n+            cufftplan.inverse(wavefront, out=wavefront)\n+\n+    elif _USE_OPENCL:\n+        if normalization is None:\n+            normalization = 1./wavefront.shape[0] if forward else wavefront.shape[0]\n+\n+        context, queue = get_opencl_context()\n+        wf_on_gpu = pyopencl.array.to_device(queue, wavefront)\n+        transform = gpyfft.fft.FFT(context, queue, wf_on_gpu, axes=(0,1))\n+        event, = transform.enqueue(forward=forward)\n+        event.wait()\n+        wavefront[:] = wf_on_gpu.get()\n+        del wf_on_gpu\n+\n+    elif _USE_FFTW:\n+        FFT_direction = 'forward' if forward else 'backward' # back compatible for use in _FFTW_INIT\n+        do_fft = pyfftw.interfaces.numpy_fft.fft2 if forward else pyfftw.interfaces.numpy_fft.ifft2\n+        if normalization is None:\n+            normalization = 1./wavefront.shape[0] if forward else wavefront.shape[0]\n+\n+\n+        if (wavefront.shape, FFT_direction) not in _FFTW_INIT:\n+            # The first time you run FFTW to transform a given size, it does a speed test to\n+            # determine optimal algorithm that is destructive to your chosen array.\n+            # So only do that test on a copy, not the real array:\n+            _log.info(\"Measuring pyfftw optimal plan for %s, direction=%s\" % (\n+                str(wavefront.shape), FFT_direction))\n+\n+            pyfftw.interfaces.cache.enable()\n+            pyfftw.interfaces.cache.set_keepalive_time(30)\n+\n+            test_array = np.zeros(wavefront.shape)\n+            test_array = do_fft(test_array, overwrite_input=True, planner_effort='FFTW_MEASURE',","path":"poppy/accel_math.py","position":255,"original_position":255,"commit_id":"a6b632861ad278931c849d343f18e6b743a74139","original_commit_id":"a6b632861ad278931c849d343f18e6b743a74139","user":{"login":"mperrin","id":1151745,"avatar_url":"https://avatars0.githubusercontent.com/u/1151745?v=4","gravatar_id":"","url":"https://api.github.com/users/mperrin","html_url":"https://github.com/mperrin","followers_url":"https://api.github.com/users/mperrin/followers","following_url":"https://api.github.com/users/mperrin/following{/other_user}","gists_url":"https://api.github.com/users/mperrin/gists{/gist_id}","starred_url":"https://api.github.com/users/mperrin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mperrin/subscriptions","organizations_url":"https://api.github.com/users/mperrin/orgs","repos_url":"https://api.github.com/users/mperrin/repos","events_url":"https://api.github.com/users/mperrin/events{/privacy}","received_events_url":"https://api.github.com/users/mperrin/received_events","type":"User","site_admin":false},"body":"I am increasingly convinced this is the case!","created_at":"2018-05-16T20:59:21Z","updated_at":"2018-05-16T20:59:21Z","html_url":"https://github.com/mperrin/poppy/pull/250#discussion_r188770743","pull_request_url":"https://api.github.com/repos/mperrin/poppy/pulls/250","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/mperrin/poppy/pulls/comments/188770743"},"html":{"href":"https://github.com/mperrin/poppy/pull/250#discussion_r188770743"},"pull_request":{"href":"https://api.github.com/repos/mperrin/poppy/pulls/250"}},"in_reply_to_id":188760489},"pull_request":{"url":"https://api.github.com/repos/mperrin/poppy/pulls/250","id":188280515,"html_url":"https://github.com/mperrin/poppy/pull/250","diff_url":"https://github.com/mperrin/poppy/pull/250.diff","patch_url":"https://github.com/mperrin/poppy/pull/250.patch","issue_url":"https://api.github.com/repos/mperrin/poppy/issues/250","number":250,"state":"open","locked":false,"title":"Unified GPU framework using either CUDA or OpenCL","user":{"login":"mperrin","id":1151745,"avatar_url":"https://avatars0.githubusercontent.com/u/1151745?v=4","gravatar_id":"","url":"https://api.github.com/users/mperrin","html_url":"https://github.com/mperrin","followers_url":"https://api.github.com/users/mperrin/followers","following_url":"https://api.github.com/users/mperrin/following{/other_user}","gists_url":"https://api.github.com/users/mperrin/gists{/gist_id}","starred_url":"https://api.github.com/users/mperrin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mperrin/subscriptions","organizations_url":"https://api.github.com/users/mperrin/orgs","repos_url":"https://api.github.com/users/mperrin/repos","events_url":"https://api.github.com/users/mperrin/events{/privacy}","received_events_url":"https://api.github.com/users/mperrin/received_events","type":"User","site_admin":false},"body":"This branch both adds support for OpenCL and refactors the CUDA implementation somewhat. \r\n\r\n- Essentially all GPU and FFTW related functionality is now relocated into `accel_math.py`, inside wrapper functions, in particular one called `fft_2d`. This allows the algorithmic code in the other files to be written without having to concern itself with _how_ a given FFT is implemented. In principle this extends and generales the `_fft` and `_inv_fft` methods on the FresnelWavefront class (which in fact just become stubs that pass through to `accel_math.fft_2d`. This change involved refactoring key algorithmic parts of both `fresnel.py` and `poppy_core.py`. \r\n- OpenCL-based implementation of GPU accelerated FFTs added.  Dependencies are pyopencl, conda, gpyfft. The first two can be installed from conda-forge. The latter is in a user contributed conda package, so use ` conda install -c ljbo3 gpyfft`. A new setting `poppy.conf.use_cuda` allows enabling or disabling this code, on machines with OpenCL-compatible GPUs. \r\n- Work in progress partial implementation of toggling between single and double precision floating point math, controlled by a new setting `poppy.conf.double_precision` which defaults to True. Many things work in single precision mode but currently many test cases fail, presumably since they're not smart enough to adjust tolerances appropriately. Best to ignore this setting for now for most purposes. \r\n\r\nAll tests pass locally for me, using any combo of plain numpy, numexpr+fftw, CUDA, or OpenCL. On the other hand this starts to get very machine-dependent in test setups so further testing is much appreciated.\r\n\r\nSome areas for future work before merging this:\r\n\r\n1. In refactoring the CUDA functionality out of the FresnelWavefront class, that lost the ability to cache CUDA FFT plans as part of FresnelWavefront objects. I tried caching them as a module-level variable in `accel_math.py`, but for unclear reasons doing so led to a memory leak on the GPU (buffers not being freed somehow?), which eventually led to calculations totally failing due to inability to malloc new buffers on the GPU. I couldn't debug where the leak was, but simply _not_ caching the plans avoids it entirely. The time to create a new CUDA FFT plan each time appears < 1 ms, so it's not zero but is relatively negligible. I'm not totally happy with this but reliability comes first. \r\n2. The single precision stuff is all use-at-your-own-risk. Needs more debugging and attention tothe unit test tolerances.\r\n3. Needs docs! \r\n4. Travis doesn't support CI for GPUs, so all the meaningful testing for this will need to be run locally. Need to investigate the available CUDA compute servers at STScI. \r\n5. Benchmark functions would be nice to include. Probably a few relevant cases, perhaps (1) bare FFT (2) coronagraphic multi-plane OpticalSystem calc in Fraunhofer regime (3) Fresnel multi-plane calc.  \r\n","created_at":"2018-05-15T23:14:02Z","updated_at":"2018-05-16T20:59:21Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8e41ca3d1970d76500eb7b63294beb9cbdf9acee","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/mperrin/poppy/pulls/250/commits","review_comments_url":"https://api.github.com/repos/mperrin/poppy/pulls/250/comments","review_comment_url":"https://api.github.com/repos/mperrin/poppy/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mperrin/poppy/issues/250/comments","statuses_url":"https://api.github.com/repos/mperrin/poppy/statuses/a6b632861ad278931c849d343f18e6b743a74139","head":{"label":"mperrin:opencl","ref":"opencl","sha":"a6b632861ad278931c849d343f18e6b743a74139","user":{"login":"mperrin","id":1151745,"avatar_url":"https://avatars0.githubusercontent.com/u/1151745?v=4","gravatar_id":"","url":"https://api.github.com/users/mperrin","html_url":"https://github.com/mperrin","followers_url":"https://api.github.com/users/mperrin/followers","following_url":"https://api.github.com/users/mperrin/following{/other_user}","gists_url":"https://api.github.com/users/mperrin/gists{/gist_id}","starred_url":"https://api.github.com/users/mperrin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mperrin/subscriptions","organizations_url":"https://api.github.com/users/mperrin/orgs","repos_url":"https://api.github.com/users/mperrin/repos","events_url":"https://api.github.com/users/mperrin/events{/privacy}","received_events_url":"https://api.github.com/users/mperrin/received_events","type":"User","site_admin":false},"repo":{"id":4206587,"name":"poppy","full_name":"mperrin/poppy","owner":{"login":"mperrin","id":1151745,"avatar_url":"https://avatars0.githubusercontent.com/u/1151745?v=4","gravatar_id":"","url":"https://api.github.com/users/mperrin","html_url":"https://github.com/mperrin","followers_url":"https://api.github.com/users/mperrin/followers","following_url":"https://api.github.com/users/mperrin/following{/other_user}","gists_url":"https://api.github.com/users/mperrin/gists{/gist_id}","starred_url":"https://api.github.com/users/mperrin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mperrin/subscriptions","organizations_url":"https://api.github.com/users/mperrin/orgs","repos_url":"https://api.github.com/users/mperrin/repos","events_url":"https://api.github.com/users/mperrin/events{/privacy}","received_events_url":"https://api.github.com/users/mperrin/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mperrin/poppy","description":"Physical Optics Propagation in Python","fork":false,"url":"https://api.github.com/repos/mperrin/poppy","forks_url":"https://api.github.com/repos/mperrin/poppy/forks","keys_url":"https://api.github.com/repos/mperrin/poppy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mperrin/poppy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mperrin/poppy/teams","hooks_url":"https://api.github.com/repos/mperrin/poppy/hooks","issue_events_url":"https://api.github.com/repos/mperrin/poppy/issues/events{/number}","events_url":"https://api.github.com/repos/mperrin/poppy/events","assignees_url":"https://api.github.com/repos/mperrin/poppy/assignees{/user}","branches_url":"https://api.github.com/repos/mperrin/poppy/branches{/branch}","tags_url":"https://api.github.com/repos/mperrin/poppy/tags","blobs_url":"https://api.github.com/repos/mperrin/poppy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mperrin/poppy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mperrin/poppy/git/refs{/sha}","trees_url":"https://api.github.com/repos/mperrin/poppy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mperrin/poppy/statuses/{sha}","languages_url":"https://api.github.com/repos/mperrin/poppy/languages","stargazers_url":"https://api.github.com/repos/mperrin/poppy/stargazers","contributors_url":"https://api.github.com/repos/mperrin/poppy/contributors","subscribers_url":"https://api.github.com/repos/mperrin/poppy/subscribers","subscription_url":"https://api.github.com/repos/mperrin/poppy/subscription","commits_url":"https://api.github.com/repos/mperrin/poppy/commits{/sha}","git_commits_url":"https://api.github.com/repos/mperrin/poppy/git/commits{/sha}","comments_url":"https://api.github.com/repos/mperrin/poppy/comments{/number}","issue_comment_url":"https://api.github.com/repos/mperrin/poppy/issues/comments{/number}","contents_url":"https://api.github.com/repos/mperrin/poppy/contents/{+path}","compare_url":"https://api.github.com/repos/mperrin/poppy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mperrin/poppy/merges","archive_url":"https://api.github.com/repos/mperrin/poppy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mperrin/poppy/downloads","issues_url":"https://api.github.com/repos/mperrin/poppy/issues{/number}","pulls_url":"https://api.github.com/repos/mperrin/poppy/pulls{/number}","milestones_url":"https://api.github.com/repos/mperrin/poppy/milestones{/number}","notifications_url":"https://api.github.com/repos/mperrin/poppy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mperrin/poppy/labels{/name}","releases_url":"https://api.github.com/repos/mperrin/poppy/releases{/id}","deployments_url":"https://api.github.com/repos/mperrin/poppy/deployments","created_at":"2012-05-02T19:09:02Z","updated_at":"2018-05-15T01:15:33Z","pushed_at":"2018-05-16T17:31:16Z","git_url":"git://github.com/mperrin/poppy.git","ssh_url":"git@github.com:mperrin/poppy.git","clone_url":"https://github.com/mperrin/poppy.git","svn_url":"https://github.com/mperrin/poppy","homepage":null,"size":29841,"stargazers_count":52,"watchers_count":52,"language":"Jupyter Notebook","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":24,"mirror_url":null,"archived":false,"open_issues_count":28,"license":null,"forks":24,"open_issues":28,"watchers":52,"default_branch":"master"}},"base":{"label":"mperrin:master","ref":"master","sha":"34ea05c8041594d3a5ab23befc16ba4b33fc87a3","user":{"login":"mperrin","id":1151745,"avatar_url":"https://avatars0.githubusercontent.com/u/1151745?v=4","gravatar_id":"","url":"https://api.github.com/users/mperrin","html_url":"https://github.com/mperrin","followers_url":"https://api.github.com/users/mperrin/followers","following_url":"https://api.github.com/users/mperrin/following{/other_user}","gists_url":"https://api.github.com/users/mperrin/gists{/gist_id}","starred_url":"https://api.github.com/users/mperrin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mperrin/subscriptions","organizations_url":"https://api.github.com/users/mperrin/orgs","repos_url":"https://api.github.com/users/mperrin/repos","events_url":"https://api.github.com/users/mperrin/events{/privacy}","received_events_url":"https://api.github.com/users/mperrin/received_events","type":"User","site_admin":false},"repo":{"id":4206587,"name":"poppy","full_name":"mperrin/poppy","owner":{"login":"mperrin","id":1151745,"avatar_url":"https://avatars0.githubusercontent.com/u/1151745?v=4","gravatar_id":"","url":"https://api.github.com/users/mperrin","html_url":"https://github.com/mperrin","followers_url":"https://api.github.com/users/mperrin/followers","following_url":"https://api.github.com/users/mperrin/following{/other_user}","gists_url":"https://api.github.com/users/mperrin/gists{/gist_id}","starred_url":"https://api.github.com/users/mperrin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mperrin/subscriptions","organizations_url":"https://api.github.com/users/mperrin/orgs","repos_url":"https://api.github.com/users/mperrin/repos","events_url":"https://api.github.com/users/mperrin/events{/privacy}","received_events_url":"https://api.github.com/users/mperrin/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mperrin/poppy","description":"Physical Optics Propagation in Python","fork":false,"url":"https://api.github.com/repos/mperrin/poppy","forks_url":"https://api.github.com/repos/mperrin/poppy/forks","keys_url":"https://api.github.com/repos/mperrin/poppy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mperrin/poppy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mperrin/poppy/teams","hooks_url":"https://api.github.com/repos/mperrin/poppy/hooks","issue_events_url":"https://api.github.com/repos/mperrin/poppy/issues/events{/number}","events_url":"https://api.github.com/repos/mperrin/poppy/events","assignees_url":"https://api.github.com/repos/mperrin/poppy/assignees{/user}","branches_url":"https://api.github.com/repos/mperrin/poppy/branches{/branch}","tags_url":"https://api.github.com/repos/mperrin/poppy/tags","blobs_url":"https://api.github.com/repos/mperrin/poppy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mperrin/poppy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mperrin/poppy/git/refs{/sha}","trees_url":"https://api.github.com/repos/mperrin/poppy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mperrin/poppy/statuses/{sha}","languages_url":"https://api.github.com/repos/mperrin/poppy/languages","stargazers_url":"https://api.github.com/repos/mperrin/poppy/stargazers","contributors_url":"https://api.github.com/repos/mperrin/poppy/contributors","subscribers_url":"https://api.github.com/repos/mperrin/poppy/subscribers","subscription_url":"https://api.github.com/repos/mperrin/poppy/subscription","commits_url":"https://api.github.com/repos/mperrin/poppy/commits{/sha}","git_commits_url":"https://api.github.com/repos/mperrin/poppy/git/commits{/sha}","comments_url":"https://api.github.com/repos/mperrin/poppy/comments{/number}","issue_comment_url":"https://api.github.com/repos/mperrin/poppy/issues/comments{/number}","contents_url":"https://api.github.com/repos/mperrin/poppy/contents/{+path}","compare_url":"https://api.github.com/repos/mperrin/poppy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mperrin/poppy/merges","archive_url":"https://api.github.com/repos/mperrin/poppy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mperrin/poppy/downloads","issues_url":"https://api.github.com/repos/mperrin/poppy/issues{/number}","pulls_url":"https://api.github.com/repos/mperrin/poppy/pulls{/number}","milestones_url":"https://api.github.com/repos/mperrin/poppy/milestones{/number}","notifications_url":"https://api.github.com/repos/mperrin/poppy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mperrin/poppy/labels{/name}","releases_url":"https://api.github.com/repos/mperrin/poppy/releases{/id}","deployments_url":"https://api.github.com/repos/mperrin/poppy/deployments","created_at":"2012-05-02T19:09:02Z","updated_at":"2018-05-15T01:15:33Z","pushed_at":"2018-05-16T17:31:16Z","git_url":"git://github.com/mperrin/poppy.git","ssh_url":"git@github.com:mperrin/poppy.git","clone_url":"https://github.com/mperrin/poppy.git","svn_url":"https://github.com/mperrin/poppy","homepage":null,"size":29841,"stargazers_count":52,"watchers_count":52,"language":"Jupyter Notebook","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":24,"mirror_url":null,"archived":false,"open_issues_count":28,"license":null,"forks":24,"open_issues":28,"watchers":52,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mperrin/poppy/pulls/250"},"html":{"href":"https://github.com/mperrin/poppy/pull/250"},"issue":{"href":"https://api.github.com/repos/mperrin/poppy/issues/250"},"comments":{"href":"https://api.github.com/repos/mperrin/poppy/issues/250/comments"},"review_comments":{"href":"https://api.github.com/repos/mperrin/poppy/pulls/250/comments"},"review_comment":{"href":"https://api.github.com/repos/mperrin/poppy/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mperrin/poppy/pulls/250/commits"},"statuses":{"href":"https://api.github.com/repos/mperrin/poppy/statuses/a6b632861ad278931c849d343f18e6b743a74139"}},"author_association":"OWNER"}}
{"actor":{"display_login":"mperrin"}}