aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2017-02-04 17:11:11 +0100
committerMatthias P. Braendli <matthias.braendli@mpb.li>2017-02-04 17:11:11 +0100
commit7545f892a6d9555ff89381618fd2ece7f0420eaf (patch)
tree0c9c57f1034f27c84f2704e42ea9cf2ae9a08ffa
parente6c48bd803be2d27a5522d7247bd8c73c1aa8c15 (diff)
downloadodr-dpd-7545f892a6d9555ff89381618fd2ece7f0420eaf.tar.gz
odr-dpd-7545f892a6d9555ff89381618fd2ece7f0420eaf.tar.bz2
odr-dpd-7545f892a6d9555ff89381618fd2ece7f0420eaf.zip
Add jupyter notebook about subsample delay experiments
-rw-r--r--align/GenerateExampleTxRxIQ.ipynb1795
-rw-r--r--align/README.md3
2 files changed, 1798 insertions, 0 deletions
diff --git a/align/GenerateExampleTxRxIQ.ipynb b/align/GenerateExampleTxRxIQ.ipynb
new file mode 100644
index 0000000..8f2fe6b
--- /dev/null
+++ b/align/GenerateExampleTxRxIQ.ipynb
@@ -0,0 +1,1795 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Generate an example RX and TX dataset, with a subsample delay and try to resolve it afterwards"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Generate signal"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": [
+ "%matplotlib notebook\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import scipy.signal\n",
+ "import numpy as np\n",
+ "\n",
+ "\n",
+ "iq_file = \"/home/bram/dab/aux/odr-dab-cir/phasereference.2048000.fc64.iq\"\n",
+ "\n",
+ "iq_data = np.fromfile(iq_file, np.complex64)\n",
+ "phase_ref_iq = scipy.signal.resample(iq_data, 2 * len(iq_data))\n",
+ "\n",
+ "noise_iq = np.random.normal(scale = np.max(np.abs(phase_ref_iq)) * 0.02,\n",
+ " size=len(phase_ref_iq))\n",
+ "\n",
+ "phase_ref_iq = phase_ref_iq + noise_iq\n",
+ "\n",
+ "# exp(-2i pi f) is the Fourier transform of a unity delay.\n",
+ "# exp(2i pi f) is a negative delay.\n",
+ "bin_frequencies = np.concatenate(\n",
+ " (np.linspace(0, 0.5, len(phase_ref_iq)/2, endpoint=False),\n",
+ " np.linspace(-0.5, 0, len(phase_ref_iq)/2, endpoint=False)))\n",
+ "\n",
+ "phase_ref_uc = scipy.signal.resample(phase_ref_iq, 4 * len(phase_ref_iq))\n",
+ "\n",
+ "do_integer_compensation = 0\n",
+ "delay = 1\n",
+ "\n",
+ "phase_ref_uc_delayed = np.roll(phase_ref_uc, delay)\n",
+ "\n",
+ "phase_ref_delayed = scipy.signal.resample(phase_ref_uc_delayed, len(phase_ref_iq))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Part 1: integer delay"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "collapsed": false,
+ "scrolled": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Integer delay corrected: 0\n"
+ ]
+ }
+ ],
+ "source": [
+ "corr_begin_ix = -32\n",
+ "corr_end_ix = 32\n",
+ "\n",
+ "corr = [np.abs(np.corrcoef(phase_ref_delayed, np.roll(phase_ref_iq, i))[0,1]) \n",
+ " for i in range(corr_begin_ix, corr_end_ix)]\n",
+ "# TODO check for negative real correlation peak\n",
+ "if do_integer_compensation:\n",
+ " delay_in = np.argmax(corr) + corr_begin_ix\n",
+ "else:\n",
+ " delay_in = 0\n",
+ "\n",
+ "phase_ref_int_delay_removed = np.roll(phase_ref_delayed, -delay_in)\n",
+ "\n",
+ "print(\"Integer delay corrected: {}\".format(delay_in))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Part 2: factional delay"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Calculate fractional delay"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "collapsed": false,
+ "scrolled": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Applying subsample correction: -0.4999999999999981\n",
+ "-3249.30731816 5104.0 -1.0 0.0 0.0\n"
+ ]
+ },
+ {
+ "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 = $('<div/>');\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",
+ " '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
+ " 'ui-helper-clearfix\"/>');\n",
+ " var titletext = $(\n",
+ " '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
+ " 'text-align: center; padding: 3px;\"/>');\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 = $('<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 = $('<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 = $('<canvas/>');\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 = $('<div/>')\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 = $('<button/>');\n",
+ " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
+ " 'ui-button-icon-only');\n",
+ " button.attr('role', 'button');\n",
+ " button.attr('aria-disabled', 'false');\n",
+ " button.click(method_name, toolbar_event);\n",
+ " button.mouseover(tooltip, toolbar_mouse_event);\n",
+ "\n",
+ " var icon_img = $('<span/>');\n",
+ " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
+ " icon_img.addClass(image);\n",
+ " icon_img.addClass('ui-corner-all');\n",
+ "\n",
+ " var tooltip_span = $('<span/>');\n",
+ " tooltip_span.addClass('ui-button-text');\n",
+ " tooltip_span.html(tooltip);\n",
+ "\n",
+ " button.append(icon_img);\n",
+ " button.append(tooltip_span);\n",
+ "\n",
+ " nav_element.append(button);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker_span = $('<span/>');\n",
+ "\n",
+ " var fmt_picker = $('<select/>');\n",
+ " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
+ " fmt_picker_span.append(fmt_picker);\n",
+ " nav_element.append(fmt_picker_span);\n",
+ " this.format_dropdown = fmt_picker[0];\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = $(\n",
+ " '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
+ " fmt_picker.append(option)\n",
+ " }\n",
+ "\n",
+ " // Add hover states to the ui-buttons\n",
+ " $( \".ui-button\" ).hover(\n",
+ " function() { $(this).addClass(\"ui-state-hover\");},\n",
+ " function() { $(this).removeClass(\"ui-state-hover\");}\n",
+ " );\n",
+ "\n",
+ " var status_bar = $('<span class=\"mpl-message\"/>');\n",
+ " nav_element.append(status_bar);\n",
+ " this.message = status_bar[0];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function(type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function() {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1]);\n",
+ " fig.send_message(\"refresh\", {});\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
+ " var x0 = msg['x0'];\n",
+ " var y0 = fig.canvas.height - msg['y0'];\n",
+ " var x1 = msg['x1'];\n",
+ " var y1 = fig.canvas.height - msg['y1'];\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0, 0, fig.canvas.width, fig.canvas.height);\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
+ " var cursor = msg['cursor'];\n",
+ " switch(cursor)\n",
+ " {\n",
+ " case 0:\n",
+ " cursor = 'pointer';\n",
+ " break;\n",
+ " case 1:\n",
+ " cursor = 'default';\n",
+ " break;\n",
+ " case 2:\n",
+ " cursor = 'crosshair';\n",
+ " break;\n",
+ " case 3:\n",
+ " cursor = 'move';\n",
+ " break;\n",
+ " }\n",
+ " fig.rubberband_canvas.style.cursor = cursor;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function() {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message(\"ack\", {});\n",
+ "}\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " evt.data.type = \"image/png\";\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src);\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " evt.data);\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig[\"handle_\" + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
+ "mpl.findpos = function(e) {\n",
+ " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
+ " var targ;\n",
+ " if (!e)\n",
+ " e = window.event;\n",
+ " if (e.target)\n",
+ " targ = e.target;\n",
+ " else if (e.srcElement)\n",
+ " targ = e.srcElement;\n",
+ " if (targ.nodeType == 3) // defeat Safari bug\n",
+ " targ = targ.parentNode;\n",
+ "\n",
+ " // jQuery normalizes the pageX and pageY\n",
+ " // pageX,Y are the mouse positions relative to the document\n",
+ " // offset() returns the position of the element relative to the document\n",
+ " var x = e.pageX - $(targ).offset().left;\n",
+ " var y = e.pageY - $(targ).offset().top;\n",
+ "\n",
+ " return {\"x\": x, \"y\": y};\n",
+ "};\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * http://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys (original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object')\n",
+ " obj[key] = original[key]\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function(event, name) {\n",
+ " var canvas_pos = mpl.findpos(event)\n",
+ "\n",
+ " if (name === 'button_press')\n",
+ " {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " var x = canvas_pos.x;\n",
+ " var y = canvas_pos.y;\n",
+ "\n",
+ " this.send_message(name, {x: x, y: y, button: event.button,\n",
+ " step: event.step,\n",
+ " guiEvent: simpleKeys(event)});\n",
+ "\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We want\n",
+ " * to control all of the cursor setting manually through the\n",
+ " * 'cursor' event from matplotlib */\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function(event, name) {\n",
+ "\n",
+ " // Prevent repeat events\n",
+ " if (name == 'key_press')\n",
+ " {\n",
+ " if (event.which === this._key)\n",
+ " return;\n",
+ " else\n",
+ " this._key = event.which;\n",
+ " }\n",
+ " if (name == 'key_release')\n",
+ " this._key = null;\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.which != 17)\n",
+ " value += \"ctrl+\";\n",
+ " if (event.altKey && event.which != 18)\n",
+ " value += \"alt+\";\n",
+ " if (event.shiftKey && event.which != 16)\n",
+ " value += \"shift+\";\n",
+ "\n",
+ " value += 'k';\n",
+ " value += event.which.toString();\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, {key: value,\n",
+ " guiEvent: simpleKeys(event)});\n",
+ " return false;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
+ " if (name == 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message(\"toolbar_button\", {name: name});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.close = function() {\n",
+ " comm.close()\n",
+ " };\n",
+ " ws.send = function(m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function(msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " // Pass the mpl event to the overriden (by mpl) onmessage function.\n",
+ " ws.onmessage(msg['content']['data'])\n",
+ " });\n",
+ " return ws;\n",
+ "}\n",
+ "\n",
+ "mpl.mpl_figure_comm = function(comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = $(\"#\" + id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm)\n",
+ "\n",
+ " function ondownload(figure, format) {\n",
+ " window.open(figure.imageObj.src);\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy,\n",
+ " ondownload,\n",
+ " element.get(0));\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element.get(0);\n",
+ " fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error(\"Failed to find cell for figure\", id, fig);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var output_index = fig.cell_info[2]\n",
+ " var cell = fig.cell_info[0];\n",
+ "\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
+ " fig.root.unbind('remove')\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable()\n",
+ " $(fig.parent_element).html('<img src=\"' + dataURL + '\">');\n",
+ " fig.close_ws(fig, msg);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function(fig, msg){\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\">';\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function() {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message(\"ack\", {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () { fig.push_to_output() }, 1000);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function() {\n",
+ " var fig = this;\n",
+ "\n",
+ " var nav_element = $('<div/>')\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) { continue; };\n",
+ "\n",
+ " var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></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 = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
+ " nav_element.append(status_bar);\n",
+ " this.message = status_bar[0];\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
+ " var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></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",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\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<ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code'){\n",
+ " for (var j=0; j<cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 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": [
+ "<IPython.core.display.Javascript object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "<img src=\"data:image/png;base64,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\">"
+ ],
+ "text/plain": [
+ "<IPython.core.display.HTML object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[<matplotlib.lines.Line2D at 0x7fea35c08780>]"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "signal_fft = np.fft.fft(phase_ref_int_delay_removed)\n",
+ "reference_fft = np.fft.fft(phase_ref_iq)\n",
+ "\n",
+ "# rotate each bin backwards with the phase of the reference. As we have already resolved the\n",
+ "# integer delay, we should find at most one 2*pi wrapping.\n",
+ "u = signal_fft * np.conj(reference_fft)\n",
+ "\n",
+ "\n",
+ "\n",
+ "# the phase signal will still wrap around, and will have values between -pi/4 and pi/4\n",
+ "phase_wrapping = np.angle(u)\n",
+ "\n",
+ "unwrap_with_deriv_integrate = False\n",
+ "if unwrap_with_deriv_integrate:\n",
+ " # to unwrap, take the derivative, remove peaks, integrate\n",
+ " phase_deriv = phase_wrapping - np.roll(phase_wrapping, 1)\n",
+ "\n",
+ " def filter_phase_deriv(p):\n",
+ " if np.abs(p) < 0.5:\n",
+ " return p\n",
+ " else:\n",
+ " return 0\n",
+ " \n",
+ " phase_deriv_nopeaks = [filter_phase_deriv(p) for p in phase_deriv]\n",
+ " phase_unwrapped = np.cumsum(phase_deriv_nopeaks)\n",
+ "else:\n",
+ " phase_unwrapped = np.mod(phase_wrapping + np.pi/2, np.ones(len(phase_wrapping)) * np.pi / 2)\n",
+ "\n",
+ "\n",
+ "# Find the slope using a linear regression\n",
+ "slope, intercept, r_value, p_value, std_err = scipy.stats.linregress(phase_unwrapped,range(len(phase_unwrapped)))\n",
+ "\n",
+ "if p_value < 0.05:\n",
+ " frac_delay = len(phase_unwrapped) / slope / np.pi\n",
+ " print(\"Applying subsample correction: {}\".format(frac_delay))\n",
+ " print(slope, intercept, r_value, p_value, std_err)\n",
+ "\n",
+ " \n",
+ " \n",
+ "else:\n",
+ " print(\"Skipping subsample correction\")\n",
+ " print(slope, intercept, r_value, p_value, std_err)\n",
+ " frac_delay = None\n",
+ "\n",
+ "plt.plot(phase_wrapping)\n",
+ "plt.plot(phase_unwrapped)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {
+ "collapsed": false,
+ "scrolled": 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 = $('<div/>');\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",
+ " '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
+ " 'ui-helper-clearfix\"/>');\n",
+ " var titletext = $(\n",
+ " '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
+ " 'text-align: center; padding: 3px;\"/>');\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 = $('<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 = $('<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 = $('<canvas/>');\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 = $('<div/>')\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 = $('<button/>');\n",
+ " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
+ " 'ui-button-icon-only');\n",
+ " button.attr('role', 'button');\n",
+ " button.attr('aria-disabled', 'false');\n",
+ " button.click(method_name, toolbar_event);\n",
+ " button.mouseover(tooltip, toolbar_mouse_event);\n",
+ "\n",
+ " var icon_img = $('<span/>');\n",
+ " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
+ " icon_img.addClass(image);\n",
+ " icon_img.addClass('ui-corner-all');\n",
+ "\n",
+ " var tooltip_span = $('<span/>');\n",
+ " tooltip_span.addClass('ui-button-text');\n",
+ " tooltip_span.html(tooltip);\n",
+ "\n",
+ " button.append(icon_img);\n",
+ " button.append(tooltip_span);\n",
+ "\n",
+ " nav_element.append(button);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker_span = $('<span/>');\n",
+ "\n",
+ " var fmt_picker = $('<select/>');\n",
+ " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
+ " fmt_picker_span.append(fmt_picker);\n",
+ " nav_element.append(fmt_picker_span);\n",
+ " this.format_dropdown = fmt_picker[0];\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = $(\n",
+ " '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
+ " fmt_picker.append(option)\n",
+ " }\n",
+ "\n",
+ " // Add hover states to the ui-buttons\n",
+ " $( \".ui-button\" ).hover(\n",
+ " function() { $(this).addClass(\"ui-state-hover\");},\n",
+ " function() { $(this).removeClass(\"ui-state-hover\");}\n",
+ " );\n",
+ "\n",
+ " var status_bar = $('<span class=\"mpl-message\"/>');\n",
+ " nav_element.append(status_bar);\n",
+ " this.message = status_bar[0];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function(type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function() {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "}\n",
+ "\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1]);\n",
+ " fig.send_message(\"refresh\", {});\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
+ " var x0 = msg['x0'];\n",
+ " var y0 = fig.canvas.height - msg['y0'];\n",
+ " var x1 = msg['x1'];\n",
+ " var y1 = fig.canvas.height - msg['y1'];\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0, 0, fig.canvas.width, fig.canvas.height);\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
+ " var cursor = msg['cursor'];\n",
+ " switch(cursor)\n",
+ " {\n",
+ " case 0:\n",
+ " cursor = 'pointer';\n",
+ " break;\n",
+ " case 1:\n",
+ " cursor = 'default';\n",
+ " break;\n",
+ " case 2:\n",
+ " cursor = 'crosshair';\n",
+ " break;\n",
+ " case 3:\n",
+ " cursor = 'move';\n",
+ " break;\n",
+ " }\n",
+ " fig.rubberband_canvas.style.cursor = cursor;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function() {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message(\"ack\", {});\n",
+ "}\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " evt.data.type = \"image/png\";\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src);\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " evt.data);\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig[\"handle_\" + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "}\n",
+ "\n",
+ "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
+ "mpl.findpos = function(e) {\n",
+ " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
+ " var targ;\n",
+ " if (!e)\n",
+ " e = window.event;\n",
+ " if (e.target)\n",
+ " targ = e.target;\n",
+ " else if (e.srcElement)\n",
+ " targ = e.srcElement;\n",
+ " if (targ.nodeType == 3) // defeat Safari bug\n",
+ " targ = targ.parentNode;\n",
+ "\n",
+ " // jQuery normalizes the pageX and pageY\n",
+ " // pageX,Y are the mouse positions relative to the document\n",
+ " // offset() returns the position of the element relative to the document\n",
+ " var x = e.pageX - $(targ).offset().left;\n",
+ " var y = e.pageY - $(targ).offset().top;\n",
+ "\n",
+ " return {\"x\": x, \"y\": y};\n",
+ "};\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * http://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys (original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object')\n",
+ " obj[key] = original[key]\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function(event, name) {\n",
+ " var canvas_pos = mpl.findpos(event)\n",
+ "\n",
+ " if (name === 'button_press')\n",
+ " {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " var x = canvas_pos.x;\n",
+ " var y = canvas_pos.y;\n",
+ "\n",
+ " this.send_message(name, {x: x, y: y, button: event.button,\n",
+ " step: event.step,\n",
+ " guiEvent: simpleKeys(event)});\n",
+ "\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We want\n",
+ " * to control all of the cursor setting manually through the\n",
+ " * 'cursor' event from matplotlib */\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function(event, name) {\n",
+ "\n",
+ " // Prevent repeat events\n",
+ " if (name == 'key_press')\n",
+ " {\n",
+ " if (event.which === this._key)\n",
+ " return;\n",
+ " else\n",
+ " this._key = event.which;\n",
+ " }\n",
+ " if (name == 'key_release')\n",
+ " this._key = null;\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.which != 17)\n",
+ " value += \"ctrl+\";\n",
+ " if (event.altKey && event.which != 18)\n",
+ " value += \"alt+\";\n",
+ " if (event.shiftKey && event.which != 16)\n",
+ " value += \"shift+\";\n",
+ "\n",
+ " value += 'k';\n",
+ " value += event.which.toString();\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, {key: value,\n",
+ " guiEvent: simpleKeys(event)});\n",
+ " return false;\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
+ " if (name == 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message(\"toolbar_button\", {name: name});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.close = function() {\n",
+ " comm.close()\n",
+ " };\n",
+ " ws.send = function(m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function(msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " // Pass the mpl event to the overriden (by mpl) onmessage function.\n",
+ " ws.onmessage(msg['content']['data'])\n",
+ " });\n",
+ " return ws;\n",
+ "}\n",
+ "\n",
+ "mpl.mpl_figure_comm = function(comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = $(\"#\" + id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm)\n",
+ "\n",
+ " function ondownload(figure, format) {\n",
+ " window.open(figure.imageObj.src);\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy,\n",
+ " ondownload,\n",
+ " element.get(0));\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element.get(0);\n",
+ " fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error(\"Failed to find cell for figure\", id, fig);\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var output_index = fig.cell_info[2]\n",
+ " var cell = fig.cell_info[0];\n",
+ "\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
+ " fig.root.unbind('remove')\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable()\n",
+ " $(fig.parent_element).html('<img src=\"' + dataURL + '\">');\n",
+ " fig.close_ws(fig, msg);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function(fig, msg){\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\">';\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function() {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message(\"ack\", {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () { fig.push_to_output() }, 1000);\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function() {\n",
+ " var fig = this;\n",
+ "\n",
+ " var nav_element = $('<div/>')\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) { continue; };\n",
+ "\n",
+ " var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></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 = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
+ " nav_element.append(status_bar);\n",
+ " this.message = status_bar[0];\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
+ " var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></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",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\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<ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code'){\n",
+ " for (var j=0; j<cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 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": [
+ "<IPython.core.display.Javascript object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "<img src=\"data:image/png;base64,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u9BzyYwAdkEjgEfTDPg3frKn9pic6vp4OO75N/CTCdWnnBSJ/lrI6ai+DuDNHN9cjErghiRipIob8c7fgHry+gD/of75e82ij5dR92RkLTVzABOJkYqa4nPRqi8RqV0mJhNvL3vwVbx78QPnAWAmMJHk5wCei4/UVI845+Gr1Y8WI1U1A/6Fj/KrPSZvO0fvAKo+5aTQr4XUlY7v6H+IdwDPjD5Ow8+UPgZ+g19q7IyvvAqvpGqoThOJkQruwg+svY+Sn4avpvw9Xift8VXC4RW81+GjgJfjizrG4LdIyEgiRirIAb4W3T4Pv6yzn5qRKLXLxLTAL3eF0xF8tfnZ1KzgzcLb2xV4Gw6v4H0GX4jUDjgLXwUc7jQmEiMVDKdmEVJb4Hm8E9IStcfjkYOPTnfF92tD8A71Zag+5SSahH4tJCwTPygcjqbq7T7R/M74iq1D+Bd2Yj0xJnHsOk0kRmN3BB8Z+ZTa9woLdwjbAa9F8/YCT1F39e5YfMd2CB9ZuTwuP5EYjd0ivD19hu/EX8XnCoWpXR6f8G1gwL/n6/A62IV35MKa4fearMLb8yJqTkgSjZEKivERps/xjsZ8/BJ7NbXH5I0HyvD7S66h9kiz6lNERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERES+pP4POCwSu/Ohy7wAAAAASUVORK5CYII=\">"
+ ],
+ "text/plain": [
+ "<IPython.core.display.HTML object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "if frac_delay:\n",
+ " fine_shift_fft = np.exp((0+1j * np.pi * frac_delay) * bin_frequencies)\n",
+ " sig_delay_removed_fft = signal_fft * fine_shift_fft\n",
+ " \n",
+ " sig_delay_removed = np.fft.ifft(sig_delay_removed_fft)\n",
+ " \n",
+ " plt.figure()\n",
+ " plt.plot(np.abs(np.fft.fftshift(np.fft.fft(phase_ref_iq))))\n",
+ " plt.plot(np.abs(np.fft.fftshift(np.fft.fft(sig_delay_removed-phase_ref_iq))))\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": false
+ },
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/align/README.md b/align/README.md
new file mode 100644
index 0000000..f335e6c
--- /dev/null
+++ b/align/README.md
@@ -0,0 +1,3 @@
+Experiments for aligning TX and RX signals with subsample accuracy.
+
+Inspired by http://www.dsprelated.com/showarticle/26.php