{"action":"created","comment":{"url":"https://api.github.com/repos/nengo/nengo/pulls/comments/87451530","pull_request_review_id":8064773,"id":87451530,"diff_hunk":"@@ -0,0 +1,969 @@\n+\"\"\"Operator graph optimizers.\"\"\"\n+\n+from collections import defaultdict\n+import logging\n+import time\n+import warnings\n+\n+import numpy as np\n+\n+from nengo.builder.neurons import SimNeurons\n+from nengo.builder import operator\n+from nengo.builder.operator import DotInc, ElementwiseInc, SlicedCopy\n+from nengo.builder.signal import Signal\n+from nengo.config import SupportRcDefaultsMixin\n+from nengo.params import BoolParam, IntParam, RcDefault\n+from nengo.utils.compat import zip_longest\n+from nengo.utils.graphs import toposort, transitive_closure\n+\n+logger = logging.getLogger(__name__)\n+\n+\n+class OpMergeOptimizer(SupportRcDefaultsMixin):\n+    \"\"\"Optimizes the operator graph by merging operators.\n+\n+    This reduces the number of iterators to iterate over in slow Python code\n+    (as opposed to fast C code) and due to the merging operations will be\n+    performed on larger chunks of sequential memory making better use of CPU\n+    caching and prefetching.\n+\n+    The optimization algorithm has a worst case complexity of O(n^2 + e) where\n+    n is the number of operators and e the number of edges in the dependency\n+    graph. In practice the run time will be much better because not all n^2\n+    pairwise combinations of operators will be evaluated. A grouping depending\n+    on the operator type and view bases will be done with dictionaries. This\n+    grouping can be done in amortized linear time and reduces the actual worst\n+    case runtime of the optimization algorithm to O(g*m^2 + e) where g is the\n+    number of groups and m the number of elements in a group. Moreover,\n+    information about memory alignment will be used to cut the inner loop short\n+    in many cases and gives a runtime much closer to linear in most cases.\n+\n+    Note that both `model` and `dg` will be changed by the `optimize` method.\n+\n+    Parameters\n+    ----------\n+    model : :class:`nengo.builder.builder.Model`\n+        Builder output to optimize.\n+    dg : dict\n+        Dict of the form {a: {b, c}} where b and c depend on a specifying the\n+        operator dependency graph of the model.\n+    \"\"\"\n+\n+    op_mergers = {}\n+\n+    enabled = BoolParam('enabled', default=True, optional=False)\n+    max_passes = IntParam('max_passes', default=None, optional=True)\n+\n+    def __init__(self, model, dg):\n+        self.enabled = RcDefault\n+        self.max_passes = RcDefault\n+\n+        self.model = model\n+        self.dg = dg\n+\n+        # Internal dict mapping Signals to Operators accessing those signals.\n+        self._sig2op = None\n+        # Internal set of Operators merged during the current optimization\n+        # pass.\n+        self._merged = None\n+\n+    @classmethod\n+    def supports_merge(cls, op_type):\n+        return op_type in cls.op_mergers\n+\n+    @classmethod\n+    def can_merge_ops(cls, op1, op2):\n+        return (\n+            type(op1) == type(op2) and\n+            type(op1) in cls.op_mergers and\n+            cls.op_mergers[type(op1)].can_merge(op1, op2))\n+\n+    @classmethod\n+    def merge_ops(cls, ops):\n+        return cls.op_mergers[type(ops[0])].merge(ops)\n+\n+    @classmethod\n+    def register_merger(cls, op_type):\n+        def register(merger):\n+            if op_type in cls.op_mergers:\n+                warnings.warn(\n+                    \"Merger for operator of type {} overwritten.\".format(\n+                        op_type))\n+            cls.op_mergers[op_type] = merger()\n+            return merger\n+        return register\n+\n+    def optimize(self):\n+        \"\"\"Perform the optimization.\"\"\"\n+        if not self.enabled:\n+            return\n+\n+        logger.info(\"Running %s ...\", self.__class__.__name__)\n+\n+        # We try first to merge operators with views only as these have a fixed\n+        # order for the memory alignment whereas operators without views could\n+        # be merged in a random order. Merging the views of operators will\n+        # propagate requirements in the memory ordering via the other\n+        # associated signals of the operator to other operators.\n+        #\n+        # Once no more operators with views can be merged, we try to merge\n+        # operators without views and then try again merging views (because\n+        # each operator merge might generate new views).\n+\n+        before, after = None, None\n+        i = 0\n+        only_merge_ops_with_view = True\n+        while only_merge_ops_with_view or after < before:\n+            if self.max_passes is not None and i >= self.max_passes:\n+                break\n+\n+            i += 1\n+            self._log_counts()\n+\n+            only_merge_ops_with_view = before is None or before != after\n+            before = len(self.dg)\n+            t_start = time.time()\n+\n+            self._perform_single_pass(only_merge_ops_with_view)\n+\n+            t_end = time.time()\n+            after = len(self.dg)\n+            logger.info(\n+                \"Pass %i [%s]: Reduced %i to %i operators in %fs.\",\n+                i, \"only views\" if only_merge_ops_with_view else \"all\",\n+                before, after, t_end - t_start)\n+\n+        self._reinitialize_model_ops(self.dg)\n+\n+    def _perform_single_pass(self, only_merge_ops_with_view):\n+        \"\"\"Perform a single optimization pass.\n+\n+        Parameters\n+        ----------\n+        only_merge_ops_with_view : bool\n+            Limits operator merges to operators with views.\n+        \"\"\"\n+        self._init_pass_helper_vars()\n+        op_replacements, sig_replacements = self._perform_merges(\n+            only_merge_ops_with_view)\n+        ops = list(op_replacements.values())\n+        self._replace_op_signals(ops, sig_replacements)\n+        self._update_dg(op_replacements)\n+        self._update_model_sigs(sig_replacements)\n+\n+    def _init_pass_helper_vars(self):\n+        \"\"\"Initializes the internal helper variables for an optimization pass.\n+        \"\"\"\n+        self._sig2op = defaultdict(list)\n+        for op in self.dg:\n+            for s in op.all_signals:\n+                self._sig2op[s].append(op)\n+        self._merged = set()\n+\n+    def _ops_by_type(self, ops):\n+        \"\"\"Returns `ops` as a dictionary sorted by type.\"\"\"\n+        by_type = defaultdict(list)\n+        for op in ops:\n+            by_type[type(op)].append(op)\n+        return by_type\n+\n+    def _log_counts(self):\n+        \"\"\"Logs for each operator type the count in the dependency graph.\"\"\"\n+        if logger.isEnabledFor(logging.DEBUG):\n+            for tp, ops in self._ops_by_type(self.dg).items():\n+                logger.debug(\"%s: %i\", tp, len(ops))\n+\n+    def _perform_merges(self, only_merge_ops_with_view):\n+        \"\"\"Go through all operators and merge them where possible.\n+\n+        Parameters\n+        ----------\n+        only_merge_ops_with_view : bool\n+            Limit merges to operators with views.\n+\n+        Returns\n+        -------\n+        op_replacements : dict\n+            Dictionary that maps from original operators to the newly merged\n+            operators. To be used to update the dependency graph.\n+        sig_replacements : dict\n+            Dictionary that maps from original signals to new signals resulting\n+            from the merges. To be used to update the signals referenced by\n+            the operators.\n+        \"\"\"\n+        step_order = toposort(self.dg)\n+        tc = transitive_closure(self.dg, step_order)","path":"nengo/builder/optimizer.py","position":195,"original_position":195,"commit_id":"08a5b4ad057baa8d4c2d68291ad6cde41322fc6b","original_commit_id":"08a5b4ad057baa8d4c2d68291ad6cde41322fc6b","user":{"login":"jgosmann","id":850157,"avatar_url":"https://avatars.githubusercontent.com/u/850157?v=3","gravatar_id":"","url":"https://api.github.com/users/jgosmann","html_url":"https://github.com/jgosmann","followers_url":"https://api.github.com/users/jgosmann/followers","following_url":"https://api.github.com/users/jgosmann/following{/other_user}","gists_url":"https://api.github.com/users/jgosmann/gists{/gist_id}","starred_url":"https://api.github.com/users/jgosmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jgosmann/subscriptions","organizations_url":"https://api.github.com/users/jgosmann/orgs","repos_url":"https://api.github.com/users/jgosmann/repos","events_url":"https://api.github.com/users/jgosmann/events{/privacy}","received_events_url":"https://api.github.com/users/jgosmann/received_events","type":"User","site_admin":false},"body":"`transitive_closure` only sorts if the second argument is not given.","created_at":"2016-11-10T18:05:30Z","updated_at":"2016-11-10T18:05:30Z","html_url":"https://github.com/nengo/nengo/pull/1035#discussion_r87451530","pull_request_url":"https://api.github.com/repos/nengo/nengo/pulls/1035","_links":{"self":{"href":"https://api.github.com/repos/nengo/nengo/pulls/comments/87451530"},"html":{"href":"https://github.com/nengo/nengo/pull/1035#discussion_r87451530"},"pull_request":{"href":"https://api.github.com/repos/nengo/nengo/pulls/1035"}}},"pull_request":{"url":"https://api.github.com/repos/nengo/nengo/pulls/1035","id":67799791,"html_url":"https://github.com/nengo/nengo/pull/1035","diff_url":"https://github.com/nengo/nengo/pull/1035.diff","patch_url":"https://github.com/nengo/nengo/pull/1035.patch","issue_url":"https://api.github.com/repos/nengo/nengo/issues/1035","number":1035,"state":"open","locked":false,"title":"Operator graph optimizer","user":{"login":"jgosmann","id":850157,"avatar_url":"https://avatars.githubusercontent.com/u/850157?v=3","gravatar_id":"","url":"https://api.github.com/users/jgosmann","html_url":"https://github.com/jgosmann","followers_url":"https://api.github.com/users/jgosmann/followers","following_url":"https://api.github.com/users/jgosmann/following{/other_user}","gists_url":"https://api.github.com/users/jgosmann/gists{/gist_id}","starred_url":"https://api.github.com/users/jgosmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jgosmann/subscriptions","organizations_url":"https://api.github.com/users/jgosmann/orgs","repos_url":"https://api.github.com/users/jgosmann/repos","events_url":"https://api.github.com/users/jgosmann/events{/privacy}","received_events_url":"https://api.github.com/users/jgosmann/received_events","type":"User","site_admin":false},"body":"I updated this description with the new template. The old description can be found below.\n\n**Description:**\nThis adds an optimizer to the simulator that goes through the operator graph and merges operators and their signals. This reduces the items to iterate over in slow Python (vs. fast NumPy) and gives larger blocks of sequentially aligned memory for improved CPU caching and prefetching.\n\n**Motivation and context:**\n\nNot everyone has a powerful GPU and can use nengo_ocl (especially at the summer school it is common to have a laptop without decent GPU). For those people this PR can significantly improve the run time.\n\nThis is also useful for running on high-performance clusters (in a serial-farming setup) like Sharcnet which mostly consist of CPU clusters and only few GPU clusters (and usually much more CPU cores than GPUs are provided).\n\nFurthermore, when using large ensemble arrays (as in SPA, especially with spaopt #941, dot products, circular convolutions) this reduces the overhead that is introduced by many small ensembles.\n\n**Interactions with other PRs:**\nNo real interactions afaik, but #941 will benefit.\n\n**How has this been tested?**\nThe optimizer is enabled by default and all the tests still pass. Furthermore, I am using the optimizer on my TCM model and on @ikajic's spa-rat model. Especially, the latter one has a large arrays of small ensembles (5018 elements in a cleanup memory) and uncovered a number of memory and runtime issues that have been fixed. @xchoo tested a slightly older version of the optimizer on Spaun. Read through the comments for quotes on run time improvements and additions to the build time (quoted build time for Spaun might be lower now with recent improvements).\n\nI did not add a lot of unit tests because for the most part it is not clear to me how to test the optimizer. It is tested through the existing tests (all those models still work) which verifies its correctness in a way, but doesn't test that it actually speed up things or isn't a no-op. Good ideas welcome.\n\n**Where should a reviewer start?**\n`optimizer.py` is the main algorithm and should be structured going from the high level to the low level of the algorithm. I tried to document it well, but it might not be sufficient to understand everything. Let me know where more comments would be helpful.\n\nMaybe it is best to go through the code with me as it is somewhat complex.\n\n**How long should this take to review?**\n\n<!--- Please estimate if this PR is a quick, average, or lengthy PR. -->\n\n<!--- Take into account both the size and complexity of the changes. -->\n\n<!--- Also note if this is a timely PR that should be reviewed by a certain date. -->\n\n<!--- Leave only the line that applies below: -->\n- Lengthy (more than 150 lines changed or changes are complicated)\n\n**Types of changes:**\n- New feature (non-breaking change which adds functionality)\n\n**Checklist:**\n\n<!--- Go over all the following points. Put an `x` in all the boxes that apply. -->\n\n<!--- If a box is not applicable, please justify below the checklist. -->\n\n<!--- If you're unsure about any of these, don't hesitate to ask. -->\n\n<!--- We're here to help! -->\n- [x] I have read the **CONTRIBUTING.rst** document.\n- [x] I have updated the documentation accordingly. (More documentation should be added, separate issue #1119.)\n- [x] I have included a changelog entry.\n- [ ] I have added tests to cover my changes.\n- [x] All new and existing tests passed.\n\n**Still to do:**\n#1119, but I think it should be a separate PR.\n# Old description for reference\n\nThis adds an optimizer to the simulator that goes through the operator graph and merges operators and their signals. This reduces the items to iterate over in slow Python (vs. fast NumPy) and gives larger blocks of sequentially aligned memory for improved CPU caching and prefetching.\n\nAt the moment the only operator that's being merged is `SimNeurons`, but I will look and see if this can be done for other operators too.\n\nThere is a trade-off, though. Running the simulation gets faster, but the build process will be slower. The algorithmic complexity of the optimizer is O(n^2 + e) where n is the number of operators and e the number of edges in the dependency graph.\n\nA few \"benchmarks\" I run:\n- Simulating an ensemble array with 100 one-dimensional ensembles with 50 neurons: Pure simulation time decreases from 18 to 4 seconds (4.5x speedup! :dash: )\n- `py.test -n 6` from 60s down to 51s\n- `py.test -n 6 --slow` from 415s down to 274s \n- `py.test` from 83s down to 71s\n\nI haven't tested in on any more complicated models yet, but that should definitely be done. It would be nice to have #937 implemented to easily see the effect on build time.\n\nIt probably would be a good idea to have an option to turn the optimization on and off. Probably with an rc setting, but I feel like this could be something people want to change per instantiation of the `Simulator`. Also wondering if there should be some more fine grained control which operators to merge (or if we add other optimizations which set of these should be done).\n\n~~I think there was an issue about adding optimizations to the simulator, but I can't find it right now.~~ Found it, this addresses #778.\n# TODO\n- [x] Merging of views has been implemented for `Copy` in the `op-opt-view` branch with a lot of code duplication. Refactor this.\n- [x] Look at view merging for other operators\n- [x] Look at general merging for other operators\n- [x] Add more documentation about how the optimization is performed and some of the finecky details (views get broken in the first pass, then fixed later, then view merging is done because first pass introduces more views)\n- [x] Could we need more than two passes? Or in other words could view merging introduce more views because of associated non-view signals? (--> only relevant if view merging is supported for other things than `Copy`)\n- [x] Add test for signal merging (this should be simple enough to unit test and is part of the public `Signal` API). Not sure if it makes sense to test the optimizer in any other way than checking that it does not break any of the existing tests. Complex interactions are hard to cover in any other way anyways. Not sure if it is worth to construct and check simple cases. Maybe that certain optimizations are done at all?\n- [x] Config options\n- [x] Under some circumstances the optimizer seems to require a lot of memory. I should investigate this.\n- [x] Always performs a minimum of two passes even if the first pass didn't lead to a reduction in the number of operators.\n- [x] This currently use the `np.stack` function in one place which requires NumPy 1.10. Replace it with an equivalent available in earlier versions. Also, add a test because that part of the code is apparently never hit in the tests (no failure on Travis-CI).\n","created_at":"2016-04-25T22:34:38Z","updated_at":"2016-11-10T18:05:30Z","closed_at":null,"merged_at":null,"merge_commit_sha":"6eb125de17c614956afd6897ae87b1c7191a9c2c","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/nengo/nengo/pulls/1035/commits","review_comments_url":"https://api.github.com/repos/nengo/nengo/pulls/1035/comments","review_comment_url":"https://api.github.com/repos/nengo/nengo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nengo/nengo/issues/1035/comments","statuses_url":"https://api.github.com/repos/nengo/nengo/statuses/08a5b4ad057baa8d4c2d68291ad6cde41322fc6b","head":{"label":"nengo:op-opt","ref":"op-opt","sha":"08a5b4ad057baa8d4c2d68291ad6cde41322fc6b","user":{"login":"nengo","id":9197486,"avatar_url":"https://avatars.githubusercontent.com/u/9197486?v=3","gravatar_id":"","url":"https://api.github.com/users/nengo","html_url":"https://github.com/nengo","followers_url":"https://api.github.com/users/nengo/followers","following_url":"https://api.github.com/users/nengo/following{/other_user}","gists_url":"https://api.github.com/users/nengo/gists{/gist_id}","starred_url":"https://api.github.com/users/nengo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nengo/subscriptions","organizations_url":"https://api.github.com/users/nengo/orgs","repos_url":"https://api.github.com/users/nengo/repos","events_url":"https://api.github.com/users/nengo/events{/privacy}","received_events_url":"https://api.github.com/users/nengo/received_events","type":"Organization","site_admin":false},"repo":{"id":9568894,"name":"nengo","full_name":"nengo/nengo","owner":{"login":"nengo","id":9197486,"avatar_url":"https://avatars.githubusercontent.com/u/9197486?v=3","gravatar_id":"","url":"https://api.github.com/users/nengo","html_url":"https://github.com/nengo","followers_url":"https://api.github.com/users/nengo/followers","following_url":"https://api.github.com/users/nengo/following{/other_user}","gists_url":"https://api.github.com/users/nengo/gists{/gist_id}","starred_url":"https://api.github.com/users/nengo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nengo/subscriptions","organizations_url":"https://api.github.com/users/nengo/orgs","repos_url":"https://api.github.com/users/nengo/repos","events_url":"https://api.github.com/users/nengo/events{/privacy}","received_events_url":"https://api.github.com/users/nengo/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nengo/nengo","description":"A Python library for creating and simulating large-scale brain models","fork":false,"url":"https://api.github.com/repos/nengo/nengo","forks_url":"https://api.github.com/repos/nengo/nengo/forks","keys_url":"https://api.github.com/repos/nengo/nengo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nengo/nengo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nengo/nengo/teams","hooks_url":"https://api.github.com/repos/nengo/nengo/hooks","issue_events_url":"https://api.github.com/repos/nengo/nengo/issues/events{/number}","events_url":"https://api.github.com/repos/nengo/nengo/events","assignees_url":"https://api.github.com/repos/nengo/nengo/assignees{/user}","branches_url":"https://api.github.com/repos/nengo/nengo/branches{/branch}","tags_url":"https://api.github.com/repos/nengo/nengo/tags","blobs_url":"https://api.github.com/repos/nengo/nengo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nengo/nengo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nengo/nengo/git/refs{/sha}","trees_url":"https://api.github.com/repos/nengo/nengo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nengo/nengo/statuses/{sha}","languages_url":"https://api.github.com/repos/nengo/nengo/languages","stargazers_url":"https://api.github.com/repos/nengo/nengo/stargazers","contributors_url":"https://api.github.com/repos/nengo/nengo/contributors","subscribers_url":"https://api.github.com/repos/nengo/nengo/subscribers","subscription_url":"https://api.github.com/repos/nengo/nengo/subscription","commits_url":"https://api.github.com/repos/nengo/nengo/commits{/sha}","git_commits_url":"https://api.github.com/repos/nengo/nengo/git/commits{/sha}","comments_url":"https://api.github.com/repos/nengo/nengo/comments{/number}","issue_comment_url":"https://api.github.com/repos/nengo/nengo/issues/comments{/number}","contents_url":"https://api.github.com/repos/nengo/nengo/contents/{+path}","compare_url":"https://api.github.com/repos/nengo/nengo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nengo/nengo/merges","archive_url":"https://api.github.com/repos/nengo/nengo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nengo/nengo/downloads","issues_url":"https://api.github.com/repos/nengo/nengo/issues{/number}","pulls_url":"https://api.github.com/repos/nengo/nengo/pulls{/number}","milestones_url":"https://api.github.com/repos/nengo/nengo/milestones{/number}","notifications_url":"https://api.github.com/repos/nengo/nengo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nengo/nengo/labels{/name}","releases_url":"https://api.github.com/repos/nengo/nengo/releases{/id}","deployments_url":"https://api.github.com/repos/nengo/nengo/deployments","created_at":"2013-04-20T18:46:06Z","updated_at":"2016-11-08T18:08:08Z","pushed_at":"2016-11-10T07:47:28Z","git_url":"git://github.com/nengo/nengo.git","ssh_url":"git@github.com:nengo/nengo.git","clone_url":"https://github.com/nengo/nengo.git","svn_url":"https://github.com/nengo/nengo","homepage":"","size":28748,"stargazers_count":180,"watchers_count":180,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":56,"mirror_url":null,"open_issues_count":244,"forks":56,"open_issues":244,"watchers":180,"default_branch":"master"}},"base":{"label":"nengo:master","ref":"master","sha":"b07a200b174a06d2ef9ba36746caf14fd0ea80b1","user":{"login":"nengo","id":9197486,"avatar_url":"https://avatars.githubusercontent.com/u/9197486?v=3","gravatar_id":"","url":"https://api.github.com/users/nengo","html_url":"https://github.com/nengo","followers_url":"https://api.github.com/users/nengo/followers","following_url":"https://api.github.com/users/nengo/following{/other_user}","gists_url":"https://api.github.com/users/nengo/gists{/gist_id}","starred_url":"https://api.github.com/users/nengo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nengo/subscriptions","organizations_url":"https://api.github.com/users/nengo/orgs","repos_url":"https://api.github.com/users/nengo/repos","events_url":"https://api.github.com/users/nengo/events{/privacy}","received_events_url":"https://api.github.com/users/nengo/received_events","type":"Organization","site_admin":false},"repo":{"id":9568894,"name":"nengo","full_name":"nengo/nengo","owner":{"login":"nengo","id":9197486,"avatar_url":"https://avatars.githubusercontent.com/u/9197486?v=3","gravatar_id":"","url":"https://api.github.com/users/nengo","html_url":"https://github.com/nengo","followers_url":"https://api.github.com/users/nengo/followers","following_url":"https://api.github.com/users/nengo/following{/other_user}","gists_url":"https://api.github.com/users/nengo/gists{/gist_id}","starred_url":"https://api.github.com/users/nengo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nengo/subscriptions","organizations_url":"https://api.github.com/users/nengo/orgs","repos_url":"https://api.github.com/users/nengo/repos","events_url":"https://api.github.com/users/nengo/events{/privacy}","received_events_url":"https://api.github.com/users/nengo/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nengo/nengo","description":"A Python library for creating and simulating large-scale brain models","fork":false,"url":"https://api.github.com/repos/nengo/nengo","forks_url":"https://api.github.com/repos/nengo/nengo/forks","keys_url":"https://api.github.com/repos/nengo/nengo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nengo/nengo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nengo/nengo/teams","hooks_url":"https://api.github.com/repos/nengo/nengo/hooks","issue_events_url":"https://api.github.com/repos/nengo/nengo/issues/events{/number}","events_url":"https://api.github.com/repos/nengo/nengo/events","assignees_url":"https://api.github.com/repos/nengo/nengo/assignees{/user}","branches_url":"https://api.github.com/repos/nengo/nengo/branches{/branch}","tags_url":"https://api.github.com/repos/nengo/nengo/tags","blobs_url":"https://api.github.com/repos/nengo/nengo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nengo/nengo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nengo/nengo/git/refs{/sha}","trees_url":"https://api.github.com/repos/nengo/nengo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nengo/nengo/statuses/{sha}","languages_url":"https://api.github.com/repos/nengo/nengo/languages","stargazers_url":"https://api.github.com/repos/nengo/nengo/stargazers","contributors_url":"https://api.github.com/repos/nengo/nengo/contributors","subscribers_url":"https://api.github.com/repos/nengo/nengo/subscribers","subscription_url":"https://api.github.com/repos/nengo/nengo/subscription","commits_url":"https://api.github.com/repos/nengo/nengo/commits{/sha}","git_commits_url":"https://api.github.com/repos/nengo/nengo/git/commits{/sha}","comments_url":"https://api.github.com/repos/nengo/nengo/comments{/number}","issue_comment_url":"https://api.github.com/repos/nengo/nengo/issues/comments{/number}","contents_url":"https://api.github.com/repos/nengo/nengo/contents/{+path}","compare_url":"https://api.github.com/repos/nengo/nengo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nengo/nengo/merges","archive_url":"https://api.github.com/repos/nengo/nengo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nengo/nengo/downloads","issues_url":"https://api.github.com/repos/nengo/nengo/issues{/number}","pulls_url":"https://api.github.com/repos/nengo/nengo/pulls{/number}","milestones_url":"https://api.github.com/repos/nengo/nengo/milestones{/number}","notifications_url":"https://api.github.com/repos/nengo/nengo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nengo/nengo/labels{/name}","releases_url":"https://api.github.com/repos/nengo/nengo/releases{/id}","deployments_url":"https://api.github.com/repos/nengo/nengo/deployments","created_at":"2013-04-20T18:46:06Z","updated_at":"2016-11-08T18:08:08Z","pushed_at":"2016-11-10T07:47:28Z","git_url":"git://github.com/nengo/nengo.git","ssh_url":"git@github.com:nengo/nengo.git","clone_url":"https://github.com/nengo/nengo.git","svn_url":"https://github.com/nengo/nengo","homepage":"","size":28748,"stargazers_count":180,"watchers_count":180,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":56,"mirror_url":null,"open_issues_count":244,"forks":56,"open_issues":244,"watchers":180,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nengo/nengo/pulls/1035"},"html":{"href":"https://github.com/nengo/nengo/pull/1035"},"issue":{"href":"https://api.github.com/repos/nengo/nengo/issues/1035"},"comments":{"href":"https://api.github.com/repos/nengo/nengo/issues/1035/comments"},"review_comments":{"href":"https://api.github.com/repos/nengo/nengo/pulls/1035/comments"},"review_comment":{"href":"https://api.github.com/repos/nengo/nengo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nengo/nengo/pulls/1035/commits"},"statuses":{"href":"https://api.github.com/repos/nengo/nengo/statuses/08a5b4ad057baa8d4c2d68291ad6cde41322fc6b"}}}}
{"actor":{"display_login":"jgosmann"}}