From 56a44d6f1b3245b67d27befc56819a9e6b9005f6 Mon Sep 17 00:00:00 2001 From: "Phyks (Lucas Verney)" Date: Wed, 2 Mar 2016 16:14:50 +0100 Subject: [PATCH] Use adaptive plotting --- Examples.ipynb | 1695 ++++++++++++++++++----------------- replot/__init__.py | 33 +- replot/adaptive_sampling.py | 162 ++++ 3 files changed, 1029 insertions(+), 861 deletions(-) create mode 100644 replot/adaptive_sampling.py diff --git a/Examples.ipynb b/Examples.ipynb index 496ee3b..9ad0a94 100644 --- a/Examples.ipynb +++ b/Examples.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 13, "metadata": { "collapsed": false }, @@ -14,11 +14,20 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 14, "metadata": { "collapsed": false }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], "source": [ "%matplotlib notebook\n", "%load_ext autoreload\n", @@ -27,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 17, "metadata": { "collapsed": false }, @@ -799,7 +808,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -812,12 +821,12 @@ "source": [ "with replot.Figure() as figure:\n", " for i in range(8):\n", - " figure.plot(lambda x: np.sin(x + np.pi * i / 4))" + " figure.plot(lambda x: np.sin(x + np.pi * i / 4), (-10, 10))" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 18, "metadata": { "collapsed": false }, @@ -1589,7 +1598,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -1606,7 +1615,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 19, "metadata": { "collapsed": false, "scrolled": false @@ -2397,7 +2406,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 20, "metadata": { "collapsed": false }, @@ -3978,7 +3987,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 21, "metadata": { "collapsed": false }, @@ -4750,7 +4759,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -4771,7 +4780,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 22, "metadata": { "collapsed": false }, @@ -5543,7 +5552,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -5558,12 +5567,12 @@ " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", " legend=\"best\") as figure:\n", - " figure.plot(np.sin, label=\"sin\")" + " figure.plot(np.sin, (-10, 10), label=\"sin\")" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 23, "metadata": { "collapsed": false }, @@ -6335,7 +6344,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -6350,12 +6359,12 @@ " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", " legend=True) as figure:\n", - " figure.plot(np.sin, label=\"sin\")" + " figure.plot(np.sin, (-10, 10), label=\"sin\")" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 24, "metadata": { "collapsed": false }, @@ -7127,7 +7136,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -7142,13 +7151,13 @@ " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", " legend=\"best\")\n", - "fig.plot(np.sin, label=\"sin\")\n", + "fig.plot(np.sin, (-10, 10), label=\"sin\")\n", "fig.show()" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 25, "metadata": { "collapsed": false }, @@ -7920,7 +7929,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -7934,12 +7943,12 @@ "with replot.Figure(xlabel=\"some x label\",\n", " ylabel=\"some y label\",\n", " title=\"A title for the figure\") as figure:\n", - " figure.plot(np.sin, label=\"sin\")" + " figure.plot(np.sin, (-10, 10), label=\"sin\")" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 26, "metadata": { "collapsed": false, "scrolled": false @@ -8712,7 +8721,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -8727,12 +8736,12 @@ " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", " legend=False) as figure:\n", - " figure.plot(np.sin, label=\"sin\")" + " figure.plot(np.sin, (-10, 10), label=\"sin\")" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 27, "metadata": { "collapsed": false, "scrolled": false @@ -9505,7 +9514,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -9520,12 +9529,12 @@ " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", " legend=True) as figure:\n", - " figure.plot(np.sin)" + " figure.plot(np.sin, (-10, 10))" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 37, "metadata": { "collapsed": false, "scrolled": true @@ -10298,7 +10307,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -10309,799 +10318,7 @@ } ], "source": [ - "replot.plot([np.sin],\n", - " xlabel=\"some x label\",\n", - " ylabel=\"some y label\",\n", - " title=\"A title for the figure\",\n", - " legend=\"best\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " this.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width);\n", - " canvas.attr('height', height);\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " event.shiftKey = false;\n", + " // Send a \"J\" for go to next cell\n", + " event.which = 74;\n", + " event.keyCode = 74;\n", + " manager.command_mode();\n", + " manager.handle_keydown(event);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" ], "text/plain": [ "" @@ -11897,12 +11906,12 @@ " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", " legend=True) as figure:\n", - " figure.plot(np.sin, linewidth=20)" + " figure.plot(np.sin, (-10, 10), linewidth=20)" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 33, "metadata": { "collapsed": false, "scrolled": false @@ -12675,7 +12684,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -12686,7 +12695,7 @@ } ], "source": [ - "replot.plot([range(10), (np.sin, (-5, 5)), np.cos, (lambda x: np.sin(x) + 4, (-10, 10), {\"linewidth\": 10}), (lambda x: np.sin(x) - 4, {\"linewidth\": 10}), ([-i for i in range(5)], {\"linewidth\": 10})],\n", + "replot.plot([range(10), (np.sin, (-5, 5)), (lambda x: np.sin(x) + 4, (-10, 10), {\"linewidth\": 10}), (lambda x: np.sin(x) - 4, (-10, 10), {\"linewidth\": 10}), ([-i for i in range(5)], {\"linewidth\": 10})],\n", " xlabel=\"some x label\",\n", " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", @@ -12696,7 +12705,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 34, "metadata": { "collapsed": false }, @@ -13468,7 +13477,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -13479,7 +13488,7 @@ } ], "source": [ - "replot.plot([range(10), (np.sin, (-5, 5)), np.cos, (lambda x: np.sin(x) + 4, (-10, 10), {\"linewidth\": 10}), (lambda x: np.sin(x) - 4, {\"linewidth\": 10}), ([-i for i in range(5)], {\"linewidth\": 10})],\n", + "replot.plot([range(10), (np.sin, (-5, 5)), (lambda x: np.sin(x) + 4, (-10, 10), {\"linewidth\": 10}), (lambda x: np.sin(x) - 4, (-10, 10), {\"linewidth\": 10}), ([-i for i in range(5)], {\"linewidth\": 10})],\n", " xlabel=\"some x label\",\n", " ylabel=\"some y label\",\n", " title=\"A title for the figure\",\n", @@ -13489,7 +13498,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 35, "metadata": { "collapsed": false }, @@ -14261,7 +14270,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -14277,9 +14286,9 @@ "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mInvalidParameterError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mreplot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFigure\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mfigure\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mfigure\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mplot\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msin\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32m/home/phyks/Code/replot/replot/__init__.py\u001b[0m in \u001b[0;36mplot\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mhasattr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m\"__call__\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 112\u001b[0m \u001b[1;31m# We want to plot a function\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 113\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_plot_function\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m*\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 114\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 115\u001b[0m \u001b[1;31m# Else, it is a point series, and we just have to store it for\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32m/home/phyks/Code/replot/replot/__init__.py\u001b[0m in \u001b[0;36m_plot_function\u001b[1;34m(self, data, *args, **kwargs)\u001b[0m\n\u001b[0;32m 145\u001b[0m raise exc.InvalidParameterError(\n\u001b[0;32m 146\u001b[0m \u001b[1;34m\"Second parameter in plot command should be a tuple \"\u001b[0m \u001b[1;33m+\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 147\u001b[1;33m \"specifying plotting interval.\")\n\u001b[0m\u001b[0;32m 148\u001b[0m \u001b[0my_values\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[0mdata\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mi\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mx_values\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 149\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mplots\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx_values\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my_values\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mreplot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFigure\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mfigure\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mfigure\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mplot\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msin\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32m/home/phyks/Code/replot/replot/__init__.py\u001b[0m in \u001b[0;36mplot\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 139\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mhasattr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m\"__call__\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 140\u001b[0m \u001b[1;31m# We want to plot a function\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 141\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_plot_function\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m*\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 142\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 143\u001b[0m \u001b[1;31m# Else, it is a point series, and we just have to store it for\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m/home/phyks/Code/replot/replot/__init__.py\u001b[0m in \u001b[0;36m_plot_function\u001b[1;34m(self, data, *args, **kwargs)\u001b[0m\n\u001b[0;32m 181\u001b[0m raise exc.InvalidParameterError(\n\u001b[0;32m 182\u001b[0m \u001b[1;34m\"Second parameter in plot command should be a tuple \"\u001b[0m \u001b[1;33m+\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 183\u001b[1;33m \"specifying plotting interval.\")\n\u001b[0m\u001b[0;32m 184\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mplots\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx_values\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my_values\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 185\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mInvalidParameterError\u001b[0m: Second parameter in plot command should be a tuple specifying plotting interval." ] } @@ -14291,7 +14300,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 36, "metadata": { "collapsed": false }, @@ -15063,7 +15072,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -15079,8 +15088,8 @@ "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mInvalidParameterError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mreplot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFigure\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mfigure\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mfigure\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mplot\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32m/home/phyks/Code/replot/replot/__init__.py\u001b[0m in \u001b[0;36mplot\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 107\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 108\u001b[0m raise exc.InvalidParameterError(\n\u001b[1;32m--> 109\u001b[1;33m \"You should pass at least one argument to this function.\")\n\u001b[0m\u001b[0;32m 110\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 111\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mhasattr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m\"__call__\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mreplot\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFigure\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mfigure\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mfigure\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mplot\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32m/home/phyks/Code/replot/replot/__init__.py\u001b[0m in \u001b[0;36mplot\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 135\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 136\u001b[0m raise exc.InvalidParameterError(\n\u001b[1;32m--> 137\u001b[1;33m \"You should pass at least one argument to this function.\")\n\u001b[0m\u001b[0;32m 138\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 139\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mhasattr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m\"__call__\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mInvalidParameterError\u001b[0m: You should pass at least one argument to this function." ] } diff --git a/replot/__init__.py b/replot/__init__.py index a3c8207..5293388 100644 --- a/replot/__init__.py +++ b/replot/__init__.py @@ -8,16 +8,12 @@ import matplotlib.pyplot as plt import numpy as np import seaborn.apionly as sns +from replot import adaptive_sampling from replot import exceptions as exc __VERSION__ = "0.0.1" -# Constants -DEFAULT_NB_SAMPLES = 1000 -DEFAULT_X_INTERVAL = np.linspace(-10, 10, - DEFAULT_NB_SAMPLES) - def mpl_custom_rc_context(): """ @@ -130,7 +126,6 @@ class Figure(): .. note:: ``kwargs`` arguments are directly passed to \ ``matplotlib.pyplot.plot``. - >>> with replot.figure() as fig: fig.plot(np.sin) >>> with replot.figure() as fig: fig.plot(np.sin, (-1, 1)) >>> with replot.figure() as fig: fig.plot(np.sin, [-1, -0.9, …, 1]) >>> with replot.figure() as fig: fig.plot([1, 2, 3], [4, 5, 6]) @@ -167,22 +162,25 @@ class Figure(): which the function should be evaluated. ``kwargs`` are passed \ directly to ``matplotlib.pyplot.plot`. """ - # TODO: Better default interval and so on, adaptive plotting if len(args) == 0: - # No interval specified, using default one - x_values = DEFAULT_X_INTERVAL - elif isinstance(args[0], (list, np.ndarray)): - # List of points specified - x_values = args[0] + # If no interval specified, raise an issue + raise exc.InvalidParameterError( + "You should pass a plotting interval to the plot command.") elif isinstance(args[0], tuple): - # Interval specified, generate a list of points - x_values = np.linspace(args[0][0], args[0][1], - DEFAULT_NB_SAMPLES) + # Interval specified, use it and adaptive plotting + x_values, y_values = adaptive_sampling.sample_function( + data, + args[0], + tol=1e-3) + elif isinstance(args[0], (list, np.ndarray)): + # List of points specified, use them and compute values of the + # function + x_values = args[0] + y_values = [data(i) for i in x_values] else: raise exc.InvalidParameterError( "Second parameter in plot command should be a tuple " + "specifying plotting interval.") - y_values = [data(i) for i in x_values] self.plots.append(((x_values, y_values) + args[1:], kwargs)) def _legend(self, axes): @@ -218,9 +216,8 @@ def plot(data, **kwargs): >>> replot.plot([range(10), (np.sin, (-5, 5)), - np.cos, (lambda x: np.sin(x) + 4, (-10, 10), {"linewidth": 10}), - (lambda x: np.sin(x) - 4, {"linewidth": 10}), + (lambda x: np.sin(x) - 4, (-10, 10), {"linewidth": 10}), ([-i for i in range(5)], {"linewidth": 10})], xlabel="some x label", ylabel="some y label", diff --git a/replot/adaptive_sampling.py b/replot/adaptive_sampling.py new file mode 100644 index 0000000..b785ae4 --- /dev/null +++ b/replot/adaptive_sampling.py @@ -0,0 +1,162 @@ +""" +Sample a 1D function to given tolerance by adaptive subdivision. + +The result of sampling is a set of points that, if plotted, produces a smooth +curve with also sharp features of the function resolved. + +This routine is useful in computing functions that are expensive to compute, +and have sharp features — it makes more sense to adaptively dedicate more +sampling points for the sharp features than the smooth parts. + +Source: http://central.scipy.org/item/53/1/adaptive-sampling-of-1d-functions + +License: Creative Commons Zero (almost public domain) http://scpyce.org/cc0 + +(Slightly) modified by Phyks (Lucas Verney). +""" +import numpy as np + + +def sample_function(func, points, tol=0.05, min_points=16, max_level=16, + sample_transform=None): + """ + Sample a 1D function to given tolerance by adaptive subdivision. + + The result of sampling is a set of points that, if plotted, + produces a smooth curve with also sharp features of the function + resolved. + + Parameters + ---------- + func : callable + Function func(x) of a single argument. It is assumed to be vectorized. + points : array-like, 1D + Initial points to sample, sorted in ascending order. + These will determine also the bounds of sampling. + tol : float, optional + Tolerance to sample to. The condition is roughly that the total + length of the curve on the (x, y) plane is computed up to this + tolerance. + min_point : int, optional + Minimum number of points to sample. + max_level : int, optional + Maximum subdivision depth. + sample_transform : callable, optional + Function w = g(x, y). The x-samples are generated so that w + is sampled. + + Returns + ------- + x : ndarray + X-coordinates + y : ndarray + Corresponding values of func(x) + + Notes + ----- + This routine is useful in computing functions that are expensive + to compute, and have sharp features --- it makes more sense to + adaptively dedicate more sampling points for the sharp features + than the smooth parts. + + Examples + -------- + >>> def func(x): + ... '''Function with a sharp peak on a smooth background''' + ... a = 0.001 + ... return x + a**2/(a**2 + x**2) + ... + >>> x, y = sample_function(func, [-1, 1], tol=1e-3) + + >>> import matplotlib.pyplot as plt + >>> xx = np.linspace(-1, 1, 12000) + >>> plt.plot(xx, func(xx), '-', x, y, '.') + >>> plt.show() + + """ + x, y = _sample_function(func, points, values=None, mask=None, depth=0, + tol=tol, + min_points=min_points, max_level=max_level, + sample_transform=sample_transform) + return (x, y[0]) + + +def _sample_function(func, points, values=None, mask=None, tol=0.05, + depth=0, min_points=16, max_level=16, + sample_transform=None): + points = np.asarray(points) + + if values is None: + values = np.atleast_2d(func(points)) + + if mask is None: + mask = Ellipsis + + if depth > max_level: + # recursion limit + return points, values + + x_a = points[..., :-1][..., mask] + x_b = points[..., 1:][..., mask] + + x_c = .5*(x_a + x_b) + y_c = np.atleast_2d(func(x_c)) + + x_2 = np.r_[points, x_c] + y_2 = np.r_['-1', values, y_c] + j = np.argsort(x_2) + + x_2 = x_2[..., j] + y_2 = y_2[..., j] + + # -- Determine the intervals at which refinement is necessary + + if len(x_2) < min_points: + mask = np.ones([len(x_2)-1], dtype=bool) + else: + # represent the data as a path in N dimensions (scaled to unit box) + if sample_transform is not None: + y_2_val = sample_transform(x_2, y_2) + else: + y_2_val = y_2 + + p = np.r_['0', + x_2[None, :], + y_2_val.real.reshape(-1, y_2_val.shape[-1]), + y_2_val.imag.reshape(-1, y_2_val.shape[-1])] + + sz = (p.shape[0]-1) // 2 + + xscale = x_2.ptp(axis=-1) + yscale = abs(y_2_val.ptp(axis=-1)).ravel() + + p[0] /= xscale + p[1:sz+1] /= yscale[:, None] + p[sz+1:] /= yscale[:, None] + + # compute the length of each line segment in the path + dp = np.diff(p, axis=-1) + s = np.sqrt((dp**2).sum(axis=0)) + s_tot = s.sum() + + # compute the angle between consecutive line segments + dp /= s + dcos = np.arccos(np.clip((dp[:, 1:] * dp[:, :-1]).sum(axis=0), -1, 1)) + + # determine where to subdivide: the condition is roughly that + # the total length of the path (in the scaled data) is computed + # to accuracy `tol` + dp_piece = dcos * .5*(s[1:] + s[:-1]) + mask = (dp_piece > tol * s_tot) + + mask = np.r_[mask, False] + mask[1:] |= mask[:-1].copy() + + # -- Refine, if necessary + + if mask.any(): + return _sample_function(func, x_2, y_2, mask, tol=tol, depth=depth+1, + min_points=min_points, max_level=max_level, + sample_transform=sample_transform) + else: + return x_2, y_2