{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Modeling of the thickness of the sensor\n", "\n", "In this notebook we will re-use the experiment done at ID28 and previously calibrated and model in 3D the detector.\n", "\n", "This detector is a Pilatus 1M with a 450µm thick silicon sensor. Let's first have a look at the absorption coefficients of this sensor material: https://physics.nist.gov/PhysRefData/XrayMassCoef/ElemTab/z14.html\n", "\n", "First we retieve the results of the previous step, then calculate the absorption efficiency:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Populating the interactive namespace from numpy and matplotlib\n" ] } ], "source": [ "%pylab nbagg" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/users/kieffer/VirtualEnvs/py3/lib/python3.5/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.\n", " from ._conv import register_converters as _register_converters\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "wavelength: 6.968e-11m,\t dist: 2.845e-01m,\t poni1: 8.865e-02m,\t poni2: 8.931e-02m,\t energy: 17.793keV\n" ] } ], "source": [ "import numpy\n", "import fabio, pyFAI, pyFAI.units, pyFAI.detectors, pyFAI.azimuthalIntegrator\n", "import json\n", "with open(\"id28.json\") as f:\n", " calib = json.load(f)\n", "\n", "thickness = 450e-6\n", "wavelength = calib[\"wavelength\"]\n", "dist = calib[\"param\"][calib['param_names'].index(\"dist\")]\n", "poni1 = calib[\"param\"][calib['param_names'].index(\"poni1\")]\n", "poni2 = calib[\"param\"][calib['param_names'].index(\"poni2\")]\n", "energy = pyFAI.units.hc/(wavelength*1e10)\n", "print(\"wavelength: %.3em,\\t dist: %.3em,\\t poni1: %.3em,\\t poni2: %.3em,\\t energy: %.3fkeV\" % \n", " (wavelength, dist, poni1, poni2, energy))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Absorption coeficient at 17.8 keV" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "µ = 1537.024214 m^-1 hence absorption efficiency for 450µm: 49.9 %\n" ] } ], "source": [ "# density from https://en.wikipedia.org/wiki/Silicon\n", "rho = 2.3290 # g/cm^3\n", "\n", "#Absorption from https://physics.nist.gov/PhysRefData/XrayMassCoef/ElemTab/z14.html\n", "# Nota: enegies are in MeV !\n", "Si_abs = \"\"\"\n", " 2.00000E-03 2.777E+03 2.669E+03 \n", " 3.00000E-03 9.784E+02 9.516E+02 \n", " 4.00000E-03 4.529E+02 4.427E+02 \n", " 5.00000E-03 2.450E+02 2.400E+02 \n", " 6.00000E-03 1.470E+02 1.439E+02 \n", " 8.00000E-03 6.468E+01 6.313E+01 \n", " 1.00000E-02 3.389E+01 3.289E+01 \n", " 1.50000E-02 1.034E+01 9.794E+00 \n", " 2.00000E-02 4.464E+00 4.076E+00 \n", " 3.00000E-02 1.436E+00 1.164E+00 \n", " 4.00000E-02 7.012E-01 4.782E-01 \n", " 5.00000E-02 4.385E-01 2.430E-01 \n", " 6.00000E-02 3.207E-01 1.434E-01 \n", " 8.00000E-02 2.228E-01 6.896E-02 \n", " 1.00000E-01 1.835E-01 4.513E-02 \n", " 1.50000E-01 1.448E-01 3.086E-02 \n", " 2.00000E-01 1.275E-01 2.905E-02 \n", " 3.00000E-01 1.082E-01 2.932E-02 \n", " 4.00000E-01 9.614E-02 2.968E-02 \n", " 5.00000E-01 8.748E-02 2.971E-02 \n", " 6.00000E-01 8.077E-02 2.951E-02 \n", " 8.00000E-01 7.082E-02 2.875E-02 \n", " 1.00000E+00 6.361E-02 2.778E-02 \n", " 1.25000E+00 5.688E-02 2.652E-02 \n", " 1.50000E+00 5.183E-02 2.535E-02 \n", " 2.00000E+00 4.480E-02 2.345E-02 \n", " 3.00000E+00 3.678E-02 2.101E-02 \n", " 4.00000E+00 3.240E-02 1.963E-02 \n", " 5.00000E+00 2.967E-02 1.878E-02 \n", " 6.00000E+00 2.788E-02 1.827E-02 \n", " 8.00000E+00 2.574E-02 1.773E-02 \n", " 1.00000E+01 2.462E-02 1.753E-02 \n", " 1.50000E+01 2.352E-02 1.746E-02 \n", " 2.00000E+01 2.338E-02 1.757E-02 \"\"\"\n", "data = numpy.array([[float(i) for i in line.split()] for line in Si_abs.split(\"\\n\") if line])\n", "energy_tab, mu_over_rho, mu_en_over_rho = data.T\n", "abs_18 = numpy.interp(energy, energy_tab*1e3, mu_en_over_rho) \n", "mu = abs_18*rho*1e+2\n", "eff = 1.0-numpy.exp(-mu*thickness)\n", "\n", "print(\"µ = %f m^-1 hence absorption efficiency for 450µm: %.1f %%\"%(mu, eff*100))\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "window.mpl = {};\n", "\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", " if (mpl.ratio != 1) {\n", " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", " }\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", " fig.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 backingStore = this.context.backingStorePixelRatio ||\n", "\tthis.context.webkitBackingStorePixelRatio ||\n", "\tthis.context.mozBackingStorePixelRatio ||\n", "\tthis.context.msBackingStorePixelRatio ||\n", "\tthis.context.oBackingStorePixelRatio ||\n", "\tthis.context.backingStorePixelRatio || 1;\n", "\n", " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\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 * mpl.ratio);\n", " canvas.attr('height', height * mpl.ratio);\n", " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\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'] / mpl.ratio;\n", " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n", " var x1 = msg['x1'] / mpl.ratio;\n", " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\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 * mpl.ratio;\n", " var y = canvas_pos.y * mpl.ratio;\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 overridden (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", " var width = fig.canvas.width/mpl.ratio\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 + '\" width=\"' + width + '\">');\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 width = this.canvas.width/mpl.ratio\n", " var dataURL = this.canvas.toDataURL();\n", " this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\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", " 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<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,iVBORw0KGgoAAAANSUhEUgAAAroAAAILCAYAAAAHaz/JAAAgAElEQVR4nOzdd3RUdf7/8ZuEGqq4wBKFQRFEsa3AWkM0wGJXUHTX8nXXFRv+rOCAoKFo1KWoiC5IVRcF2QXLAKEZegsg0gLSIbRQkgAhbWZevz+QKyHcNHJzZybPxzn3nOVm7twP8P7+fk/GO/caAgAAAEKQ4fQCAAAAADsQugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC4AAABCEqELAACAkEToAgAAICQRugAAAAhJhC6AchcTEyOXy5Vv35NPPinDyP//JMXFxckwDO3YsaP8Focyxd8hACcRugDKxP79+/X666+rVatWqlmzpmrVqqXLLrtMjzzyiP73v//le21FC12fz6fjx4+f13scP35c/fr10z333KNGjRrJMAx16tTpnK/dsWOHDMModPvPf/5TrPOuXbtWDz/8sJo0aaJq1arJ5XLpscce07p164p1fFn/HW7YsEGGYeiuu+4q9HU//PCDDMPQq6++WibnBRCcCF0A523nzp1q0KCBqlWrpn/+858aPny4hg8frldeeUXXXnut7rnnnnyvz8nJUXZ2dr595wrdvLw8ZWVlye/32/57KGsbN27UCy+8oGbNmik8PFyGYSgyMlI33XSThgwZUuLwPR2vjRo10r333lto6J44cUJfffXVObc6deqoSpUqOnToUJHnXLVqlapVq6aoqCj1799fo0ePltvtVp06dVS9enWtX7++yPew4x8rN954oyIiIrR3717L19x///0yDEMbNmwos/MCCD6ELoDz9uKLL8owDH333Xfn/Pn+/fuLfI9zhW4wys3N1csvv6zw8HC1atVKffv21eTJkzV9+nSNHz9ezzzzjP7whz/ooosuUkJCQrHfNzs7WykpKeavCwtdK0uWLJFhGHrooYeK9fqnnnpKhmFozZo1+fZPnTpVhmHI7XYX+R52hO6oUaNkGIbi4+PP+fODBw+qUqVKuvHGG8vsnACCU/D//yoAHNepUycZhlHsTynP99KFjIwMvfnmm2rZsqWqVq2qevXq6ZZbbtE333yT73Xr16/Xgw8+qAsvvFBVqlRRixYt1L9//wKfJp8+z6ZNm9S7d29ddNFFqlKliq655hpNmzateH8Ikrxer+68807Vrl27wFrOXn/37t1VpUqVEr3/mUoTuqfDtbiB/dBDD8kwDKWmpubbv3z5chmGoQEDBhT5Huf6O/T5fHrppZdkGIbi4uLyvX727Nnq2LGj6tSpo6pVq+rqq6/Wv//973yvOX78uGrWrKnmzZuf85yDBg2SYRgaNWpUsX6fAEIXoQvgvL3wwgsyDENDhw4t1mUG5xO6aWlpatWqlQzD0IMPPqiPP/5YQ4cO1RNPPKHHHnvMfN2qVavMa4V79eql4cOHm//J/4477pDP5ytwnhtuuEG33nqrPvzwQ33wwQdq1KiRKleuXOxPI/v06aPatWsX+M/lJ06cUF5enqRTn8wePXpUkvT222/rggsuKBCSxVHS0D0dhy6XK9/vvTCnPznt1KmTli9frpSUFCUmJur6669X48aNi/VJ/dl/h1lZWXrwwQcVERFRIERHjhypsLAw3XDDDfrXv/6lzz77TJ07d5ZhGOrRo0e+156O9gULFhQ455VXXqkaNWro2LFjxfp9AghdhC6A87Zt2zbVrl1bhmGocePGevTRR/Xhhx9q1apV53z9+YTu888/L8Mw9OmnnxZ43zMD7pZbblF4eHiBNXTr1k2GYWjChAkFznP33XfnC/UVK1bIMAz16tWryD+DvXv3qlq1avriiy/MfUuWLNG1114rwzBUuXJlPfroo/roo4/M37vP51OrVq00cODAIt//bCUN3dGjR8swDPXr16/Yx/h8PvXt21c1a9bM90W2W2+9VQcOHCjWe5z5d3j06FHdeuutqlGjhjweT77X7du3T1WrVlXXrl0LvMdLL72k8PBwbdu2zdy3ePFiGYahv//97/leu3TpUhmGoX/84x/F/n0CCF2ELoAysWPHDnXv3l2NGzfOF0VXX321Vq5cme+1pQ1dn8+nCy64QC1atCj0k+ODBw/KMAzde++9BX62e/du89Pgs88za9asAq+vWbNmvtdaGTZsmC6++GJ5vV5J0qFDh1SvXj1de+21+uKLLzR58mR16dJFNWrUyPd779evn2699dYi3/9sJQ3dG2+8UeHh4dq1a1eJzjN8+HB16tRJn376qb777jv169dPtWrVUtu2bZWenl7k8af/bOfNm6crrrhC9evX14oVKwq8btiwYeZlFYcOHcq3zZ49W4ZhaOTIkfmOueKKK1SjRo18l8yc/ofMokWLSvT7BBCaCF0AZW7fvn369ttvzUsF/vjHP+rIkSPmz0sbuqcD9lyf+p1p2bJlhX4SW6dOHf3pT38qcJ6tW7cWeK3L5dJtt91W6Pkk6bHHHtMTTzxh/nrIkCGqVauWDh8+nO91N910U77f+4gRIyyvNS1MSUL39C25SnpN7+k7LJx9icKMGTNkGIZ69+5d5Huc/rOtXbu2qlWrpk2bNp3zdac/qS9sO/ua4MGDB8swDI0ePVqSlJmZqdq1a+vyyy8v0e8TQOgidAHY6tFHH5VhGPrqq6/MfYEauue6FtflcikmJqbQ80lSx44d853vhRdeUNu2bQu8rkePHvl+771799bNN99c5PufrSTh+tprr8kwDE2ePLnY75+bm6sqVaoUuDXcabVq1SrWXQ1O/9k+++yzMgxDjz32mPmp95mee+45GYahcePGafbs2efczrx0QZJSU1NVuXJl88/viy++kGEY+te//lXs3yeA0EboArDV6f8k/f7775v77L50ITU11fLShT179hS4xVZZhG7Xrl313HPPmb9+5513VL9+feXk5OR73b333mv+3k+cOKGLLrrI1mt0c3JyVL9+fdWvX1+5ubnFfv99+/bJMAzdeeedBX7m9/sVGRmpNm3aFPk+Z/7Znv4EtmvXruaX804bMmSIDMPQDz/8UOw1StKDDz5o3jEjJiZGlSpVKvb1wwBCH6EL4LwlJibq5MmTBfb7fD517NixQMCcz5fRTt/hYcSIEQXOd2b83nLLLYqIiChwD9jTnxx+/fXXhZ7ntOKGbv/+/XXNNdeYv960aZOqVKmirl276pdfftHWrVs1cOBAhYeH6+KLL9bs2bPVpk0bXX755crIyCjy/c9W3NCdPHmyDMPQa6+9ZvmazMxMJScna9++feY+n8+nCy+8UDVq1ND27dvzvX7ixIkyDEMvvPBCkec/+8/2k08+UVhYmB544IF8/wjYs2ePqlat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\" width=\"639.8333142648146\">" ], "text/plain": [ "<IPython.core.display.HTML object>" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "Text(0.5,1,'Silicon @ 17.8 keV')" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "depth = numpy.linspace(0, 1000, 100)\n", "res = numpy.exp(-mu*depth*1e-6)\n", "fig, ax = subplots()\n", "ax.plot(depth, res, \"-\")\n", "ax.set_xlabel(\"Depth (µm)\")\n", "ax.set_ylabel(\"Residual signal\")\n", "ax.set_title(\"Silicon @ 17.8 keV\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is consistent with:\n", "http://henke.lbl.gov/optical_constants/filter2.html\n", "\n", "Now we can model the detector\n", "\n", "## Modeling of the detector:\n", "\n", "The detector is seen as a 2D array of voxel. Let vox, voy and voz be the dimention of the detector in the three dimentions.\n", "\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Detector Pilatus 1M\t PixelSize= 1.720e-04, 1.720e-04 m\n", "0.000172 0.000172 0.00045\n" ] } ], "source": [ "detector= pyFAI.detector_factory(calib[\"detector\"])\n", "print(detector)\n", "\n", "vox = detector.pixel2 # this is not a typo\n", "voy = detector.pixel1 # x <--> axis 2\n", "voz = thickness\n", "\n", "print(vox, voy, voz)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The intensity grabbed in this voxel is the triple integral of the absorbed signal coming from this pixel or from the neighboring ones.\n", "\n", "There are 3 ways to perform this intergral:\n", "* Volumetric analytic integral. Looks feasible with a change of variable in the depth\n", "* Slice per slice, the remaining intensity depand on the incidence angle + pixel splitting between neighbooring pixels\n", "* raytracing: the decay can be solved analytically for each ray, one has to throw many ray to average out the signal.\n", "\n", "For sake of simplicity, this integral will be calculated numerically using this raytracing algorithm.\n", "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.42.3443&rep=rep1&type=pdf\n", "\n", "Knowing the input position for a X-ray on the detector and its propagation vector, this algorithm allows us to calculate the length of the path in all voxel it crosses in a fairly efficient way.\n", "\n", "To speed up the calculation, we will use a few tricks:\n", "* One ray never crosses more than 16 pixels, which is reasonable considering the incidance angle \n", "* we use numba to speed-up the calculation of loops in python\n", "* We will allocate the needed memory by chuncks of 1 million elements\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from numba import jit \n", "\n", "BLOCK_SIZE = 1<<20 # 1 milion\n", "BUFFER_SIZE = 16 \n", "BIG = numpy.finfo(numpy.float32).max\n", "\n", "mask = numpy.load(\"mask.npy\").astype(numpy.int8)\n", "from scipy.sparse import csr_matrix, csc_matrix, linalg" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(array([0, 0, 1, 1], dtype=int32), array([0, 1, 1, 2], dtype=int32), array([0.00029791, 0.00029791, 0.00059583, 0.00059583], dtype=float32))\n", "The slowest run took 9.46 times longer than the fastest. This could mean that an intermediate result is being cached.\n", "10.1 µs ± 8.53 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", "10.3 µs ± 9.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)\n" ] } ], "source": [ "@jit\n", "def calc_one_ray(entx, enty, \n", " kx, ky, kz,\n", " vox, voy, voz):\n", " \"\"\"For a ray, entering at position (entx, enty), with a propagation vector (kx, ky,kz),\n", " calculate the length spent in every voxel where energy is deposited from a bunch of photons comming in the detector \n", " at a given position and and how much energy they deposit in each voxel. \n", " \n", " Direct implementation of http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.42.3443&rep=rep1&type=pdf\n", " \n", " :param entx, enty: coordinate of the entry point in meter (2 components, x,y)\n", " :param kx, ky, kz: vector with the direction of the photon (3 components, x,y,z)\n", " :param vox, voy, voz: size of the voxel in meter (3 components, x,y,z)\n", " :return: coordinates voxels in x, y and length crossed when leaving the associated voxel\n", " \"\"\"\n", " array_x = numpy.empty(BUFFER_SIZE, dtype=numpy.int32)\n", " array_x[:] = -1\n", " array_y = numpy.empty(BUFFER_SIZE, dtype=numpy.int32)\n", " array_y[:] = -1\n", " array_len = numpy.empty(BUFFER_SIZE, dtype=numpy.float32)\n", " \n", " #normalize the input propagation vector\n", " n = numpy.sqrt(kx*kx + ky*ky + kz*kz)\n", " kx /= n\n", " ky /= n\n", " kz /= n\n", " \n", " assert kz>0\n", " step_X = -1 if kx<0.0 else 1\n", " step_Y = -1 if ky<0.0 else 1\n", " \n", " assert vox>0\n", " assert voy>0\n", " assert voz>0\n", " \n", " X = int(entx//vox)\n", " Y = int(enty//voy)\n", " \n", " if kx>0.0:\n", " t_max_x = ((entx//vox+1)*(vox)-entx)/ kx\n", " elif kx<0.0:\n", " t_max_x = ((entx//vox)*(vox)-entx)/ kx\n", " else:\n", " t_max_x = BIG\n", "\n", " if ky>0.0:\n", " t_max_y = ((enty//voy+1)*(voy)-enty)/ ky\n", " elif ky<0.0:\n", " t_max_y = ((enty//voy)*(voy)-enty)/ ky\n", " else:\n", " t_max_y = BIG\n", " \n", " #Only one case for z as the ray is travelling in one direction only\n", " t_max_z = voz / kz\n", " \n", " t_delta_x = abs(vox/kx) if kx!=0 else BIG\n", " t_delta_y = abs(voy/ky) if ky!=0 else BIG\n", " t_delta_z = voz/kz\n", " \n", " finished = False\n", " last_id = 0\n", " array_x[last_id] = X\n", " array_y[last_id] = Y\n", " \n", " while not finished:\n", " if t_max_x < t_max_y:\n", " if t_max_x < t_max_z:\n", " array_len[last_id] = t_max_x\n", " last_id+=1\n", " X += step_X\n", " array_x[last_id] = X\n", " array_y[last_id] = Y\n", " t_max_x += t_delta_x\n", " else:\n", " array_len[last_id] = t_max_z\n", " finished = True\n", " else:\n", " if t_max_y < t_max_z:\n", " array_len[last_id] = t_max_y\n", " last_id+=1\n", " Y += step_Y\n", " array_x[last_id] = X\n", " array_y[last_id] = Y \n", " t_max_y += t_delta_y\n", " else:\n", " array_len[last_id] = t_max_z\n", " finished = True\n", " if last_id>=array_len.size-1:\n", " print(\"resize arrays\")\n", " old_size = len(array_len)\n", " new_size = (old_size//BUFFER_SIZE+1)*BUFFER_SIZE\n", " new_array_x = numpy.empty(new_size, dtype=numpy.int32)\n", " new_array_x[:] = -1\n", " new_array_y = numpy.empty(new_size, dtype=numpy.int32)\n", " new_array_y[:] = -1\n", " new_array_len = numpy.empty(new_size, dtype=numpy.float32)\n", " new_array_x[:old_size] = array_x\n", " new_array_y[:old_size] = array_y\n", " new_array_len[:old_size] = array_len\n", " array_x = new_array_x\n", " array_y = new_array_y\n", " array_len = new_array_len\n", " return array_x[:last_id], array_y[:last_id], array_len[:last_id]\n", "\n", "print(calc_one_ray(0.0,0.0, 1,1,1, 172e-6, 172e-6, 450e-6))\n", "import random\n", "%timeit calc_one_ray(10+random.random(),11+random.random(),\\\n", " random.random()-0.5,random.random()-0.5,0.5+random.random(), \\\n", " vox, voy, voz)\n", "%timeit calc_one_ray.py_func(10+random.random(),11+random.random(),\\\n", " random.random()-0.5,random.random()-0.5,0.5+random.random(), \\\n", " vox, voy, voz)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we are able to perform raytracing for any ray comming in the detector, we can calculate the contribution to the neighboring pixels, using the absorption law (the length travelled is already known). \n", "To average-out the signal, we will sample a few dozens of rays per pixel to get an approximatation of the volumic integrale. \n", "\n", "Now we need to store the results so that this transformation can be represented as a sparse matrix multiplication:\n", "\n", "b = M.a\n", "\n", "Where b is the recorded image (blurred) and a is the \"perfect\" signal. \n", "M being the sparse matrix where every pixel of a gives a limited number of contribution to b.\n", "\n", "Each pixel in *b* is represented by one line in *M* and we store the indices of *a* of interest with the coefficients of the matrix.\n", "So if a pixel i,j contributes to (i,j), (i+1,j), (i+1,j+1), there are only 3 elements in the line. \n", "This is advantagous for storage.\n", "\n", "We will use the CSR sparse matrix representation:\n", "https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_row_.28CSR.2C_CRS_or_Yale_format.29\n", "where there are 3 arrays:\n", "* data: containing the actual non zero values\n", "* indices: for a given line, it contains the column number of the assocated data (at the same indice)\n", "* idptr: this array contains the index of the start of every line.\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "from numba import jitclass, int8, int32, int64, float32, float64\n", "spec = [(\"vox\",float64),(\"voy\",float64),(\"voz\",float64),(\"mu\",float64),\n", " (\"dist\",float64),(\"poni1\",float64),(\"poni2\",float64),\n", " (\"width\", int64),(\"height\", int64),(\"mask\", int8[:,:]),\n", " (\"sampled\", int64), (\"data\", float32[:]),(\"indices\", int32[:]),(\"idptr\", int32[:]),\n", " ]\n", "@jitclass(spec)\n", "class ThickDetector(object):\n", " \"Calculate the point spread function as function of the geometry of the experiment\"\n", " \n", " def __init__(self, vox, voy, thickness, mask, mu, \n", " dist, poni1, poni2):\n", " \"\"\"Constructor of the class:\n", " \n", " :param vox, voy: detector pixel size in the plane\n", " :param thickness: thickness of the sensor in meters\n", " :param mask: \n", " :param mu: absorption coefficient of the sensor material\n", " :param dist: sample detector distance as defined in the geometry-file\n", " :param poni1, poni2: coordinates of the PONI as defined in the geometry \n", " \"\"\"\n", " self.vox = vox\n", " self.voy = voy\n", " self.voz = thickness\n", " self.mu = mu\n", " self.dist=dist\n", " self.poni1 = poni1\n", " self.poni2 = poni2\n", " self.width = mask.shape[-1]\n", " self.height = mask.shape[0]\n", " self.mask = mask\n", " self.sampled = 0\n", " self.data = numpy.zeros(BLOCK_SIZE, dtype=numpy.float32)\n", " self.indices = numpy.zeros(BLOCK_SIZE,dtype=numpy.int32)\n", " self.idptr = numpy.zeros(self.width*self.height+1, dtype=numpy.int32)\n", " \n", " def calc_one_ray(self, entx, enty):\n", " \"\"\"For a ray, entering at position (entx, enty), with a propagation vector (kx, ky,kz),\n", " calculate the length spent in every voxel where energy is deposited from a bunch of photons comming in the detector \n", " at a given position and and how much energy they deposit in each voxel. \n", "\n", " Direct implementation of http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.42.3443&rep=rep1&type=pdf\n", "\n", " :param entx, enty: coordinate of the entry point in meter (2 components, x,y)\n", " :return: coordinates voxels in x, y and length crossed when leaving the associated voxel\n", " \"\"\"\n", " array_x = numpy.empty(BUFFER_SIZE, dtype=numpy.int32)\n", " array_x[:] = -1\n", " array_y = numpy.empty(BUFFER_SIZE, dtype=numpy.int32)\n", " array_y[:] = -1\n", " array_len = numpy.empty(BUFFER_SIZE, dtype=numpy.float32)\n", "\n", " #normalize the input propagation vector\n", " kx = entx - self.poni2\n", " ky = enty - self.poni1\n", " kz = self.dist\n", " n = numpy.sqrt(kx*kx + ky*ky + kz*kz)\n", " kx /= n\n", " ky /= n\n", " kz /= n\n", "\n", " step_X = -1 if kx<0.0 else 1\n", " step_Y = -1 if ky<0.0 else 1\n", "\n", " X = int(entx/self.vox)\n", " Y = int(enty/self.voy)\n", "\n", " if kx>0.0:\n", " t_max_x = ((entx//self.vox+1)*(self.vox)-entx)/ kx\n", " elif kx<0.0:\n", " t_max_x = ((entx//self.vox)*(self.vox)-entx)/ kx\n", " else:\n", " t_max_x = BIG\n", "\n", " if ky>0.0:\n", " t_max_y = ((enty//self.voy+1)*(self.voy)-enty)/ ky\n", " elif ky<0.0:\n", " t_max_y = ((enty//self.voy)*(self.voy)-enty)/ ky\n", " else:\n", " t_max_y = BIG\n", "\n", " #Only one case for z as the ray is travelling in one direction only\n", " t_max_z = self.voz / kz\n", "\n", " t_delta_x = abs(self.vox/kx) if kx!=0 else BIG\n", " t_delta_y = abs(self.voy/ky) if ky!=0 else BIG\n", " t_delta_z = self.voz/kz\n", "\n", " finished = False\n", " last_id = 0\n", " array_x[last_id] = X\n", " array_y[last_id] = Y\n", "\n", " while not finished:\n", " if t_max_x < t_max_y:\n", " if t_max_x < t_max_z:\n", " array_len[last_id] = t_max_x\n", " last_id+=1\n", " X += step_X\n", " array_x[last_id] = X\n", " array_y[last_id] = Y\n", " t_max_x += t_delta_x\n", " else:\n", " array_len[last_id] = t_max_z\n", " last_id+=1\n", " finished = True\n", " else:\n", " if t_max_y < t_max_z:\n", " array_len[last_id] = t_max_y\n", " last_id+=1\n", " Y += step_Y\n", " array_x[last_id] = X\n", " array_y[last_id] = Y \n", " t_max_y += t_delta_y\n", " else:\n", " array_len[last_id] = t_max_z\n", " last_id+=1\n", " finished = True\n", " if last_id>=array_len.size-1:\n", " print(\"resize arrays\")\n", " old_size = len(array_len)\n", " new_size = (old_size//BUFFER_SIZE+1)*BUFFER_SIZE\n", " new_array_x = numpy.empty(new_size, dtype=numpy.int32)\n", " new_array_x[:] = -1\n", " new_array_y = numpy.empty(new_size, dtype=numpy.int32)\n", " new_array_y[:] = -1\n", " new_array_len = numpy.empty(new_size, dtype=numpy.float32)\n", " new_array_x[:old_size] = array_x\n", " new_array_y[:old_size] = array_y\n", " new_array_len[:old_size] = array_len\n", " array_x = new_array_x\n", " array_y = new_array_y\n", " array_len = new_array_len\n", " return array_x[:last_id], array_y[:last_id], array_len[:last_id]\n", "\n", " def one_pixel(self, row, col, sample):\n", " \"\"\"calculate the contribution of one pixel to the sparse matrix and populate it.\n", "\n", " :param row: row index of the pixel of interest\n", " :param col: column index of the pixel of interest\n", " :param sample: Oversampling rate, 10 will thow 10x10 ray per pixel\n", "\n", " :return: the extra number of pixel allocated\n", " \"\"\"\n", " if self.mask[row, col]:\n", " return (numpy.empty(0, dtype=numpy.int32),\n", " numpy.empty(0, dtype=numpy.float32))\n", "\n", " counter = 0\n", " tmp_size = 0\n", " last_buffer_size = BUFFER_SIZE\n", " tmp_idx = numpy.empty(last_buffer_size, dtype=numpy.int32)\n", " tmp_idx[:] = -1\n", " tmp_coef = numpy.zeros(last_buffer_size, dtype=numpy.float32)\n", "\n", " pos = row * self.width + col\n", " start = self.idptr[pos]\n", " for i in range(sample):\n", " posx = (col+1.0*i/sample)*vox\n", " for j in range(sample):\n", " posy = (row+1.0*j/sample)*voy\n", " array_x, array_y, array_len = self.calc_one_ray(posx, posy)\n", "\n", " rem = 1.0\n", " for i in range(array_x.size):\n", " x = array_x[i]\n", " y = array_y[i]\n", " l = array_len[i]\n", " if (x<0) or (y<0) or (y>=self.height) or (x>=self.width):\n", " break\n", " elif (self.mask[y, x]):\n", " continue\n", " idx = x + y*self.width\n", " dos = numpy.exp(-self.mu*l)\n", " value = rem - dos\n", " rem = dos\n", " for j in range(last_buffer_size):\n", " if tmp_size >= last_buffer_size:\n", " #Increase buffer size\n", " new_buffer_size = last_buffer_size + BUFFER_SIZE\n", " new_idx = numpy.empty(new_buffer_size, dtype=numpy.int32)\n", " new_coef = numpy.zeros(new_buffer_size, dtype=numpy.float32)\n", " new_idx[:last_buffer_size] = tmp_idx\n", " new_idx[last_buffer_size:] = -1\n", " new_coef[:last_buffer_size] = tmp_coef\n", " last_buffer_size = new_buffer_size\n", " tmp_idx = new_idx\n", " tmp_coef = new_coef\n", "\n", " if tmp_idx[j] == idx:\n", " tmp_coef[j] += value\n", " break\n", " elif tmp_idx[j] < 0:\n", " tmp_idx[j] = idx\n", " tmp_coef[j] = value\n", " tmp_size +=1\n", " break \n", " return tmp_idx[:tmp_size], tmp_coef[:tmp_size]\n", "\n", " def calc_csr(self, sample):\n", " \"\"\"Calculate the CSR matrix for the whole image\n", " :param sample: Oversampling factor\n", " :return: CSR matrix\n", " \"\"\"\n", " size = self.width * self.height\n", " allocated_size = BLOCK_SIZE\n", " idptr = numpy.zeros(size+1, dtype=numpy.int32) \n", " indices = numpy.zeros(allocated_size, dtype=numpy.int32)\n", " data = numpy.zeros(allocated_size, dtype=numpy.float32)\n", " self.sampled = sample*sample\n", " pos = 0\n", " start = 0\n", " for row in range(self.height):\n", " for col in range(self.width): \n", " line_idx, line_coef = self.one_pixel(row, col, sample)\n", " line_size = line_idx.size\n", " if line_size == 0:\n", " new_size = 0\n", " pos+=1\n", " idptr[pos] = start\n", " continue\n", "\n", " stop = start + line_size\n", " \n", " if stop >= allocated_size:\n", " new_buffer_size = allocated_size + BLOCK_SIZE\n", " new_idx = numpy.zeros(new_buffer_size, dtype=numpy.int32)\n", " new_coef = numpy.zeros(new_buffer_size, dtype=numpy.float32)\n", " new_idx[:allocated_size] = indices\n", " new_coef[:allocated_size] = data\n", " allocated_size = new_buffer_size\n", " indices = new_idx\n", " data = new_coef\n", "\n", " indices[start:stop] = line_idx\n", " data[start:stop] = line_coef\n", " pos+=1\n", " idptr[pos] = stop\n", " start = stop\n", " \n", " last = idptr[-1]\n", " self.data = data\n", " self.indices = indices\n", " self.idptr = idptr\n", " return (self.data[:last]/self.sampled, indices[:last], idptr)\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 2.04 s, sys: 24 ms, total: 2.06 s\n", "Wall time: 2.06 s\n" ] }, { "data": { "text/plain": [ "(array([0., 0., 0., ..., 0., 0., 0.], dtype=float32),\n", " array([ 2, 2, 4, ..., 1023180, 1023181, 1023182],\n", " dtype=int32),\n", " array([ 0, 0, 0, ..., 1902581, 1902582, 1902583],\n", " dtype=int32))" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "thick = ThickDetector(vox,voy, thickness=thickness, mu=mu, dist=dist, poni1=poni1, poni2=poni2, mask=mask)\n", "%time thick.calc_csr(1)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 20.6 s, sys: 152 ms, total: 20.8 s\n", "Wall time: 20.6 s\n" ] } ], "source": [ "thick = ThickDetector(vox,voy, thickness=thickness, mu=mu, dist=dist, poni1=poni1, poni2=poni2, mask=mask)\n", "%time pre_csr = thick.calc_csr(8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Validation of the CSR matrix obtained:\n", "\n", "For this we will build a simple 2D image with one pixel in a regular grid and calculate the effect of the transformation calculated previously on it. " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "window.mpl = {};\n", "\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", " if (mpl.ratio != 1) {\n", " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", " }\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", " fig.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 backingStore = this.context.backingStorePixelRatio ||\n", "\tthis.context.webkitBackingStorePixelRatio ||\n", "\tthis.context.mozBackingStorePixelRatio ||\n", "\tthis.context.msBackingStorePixelRatio ||\n", "\tthis.context.oBackingStorePixelRatio ||\n", "\tthis.context.backingStorePixelRatio || 1;\n", "\n", " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\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 * mpl.ratio);\n", " canvas.attr('height', height * mpl.ratio);\n", " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\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'] / mpl.ratio;\n", " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n", " var x1 = msg['x1'] / mpl.ratio;\n", " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\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 * mpl.ratio;\n", " var y = canvas_pos.y * mpl.ratio;\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 overridden (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", " var width = fig.canvas.width/mpl.ratio\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 + '\" width=\"' + width + '\">');\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 width = this.canvas.width/mpl.ratio\n", " var dataURL = this.canvas.toDataURL();\n", " this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\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", " 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<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,iVBORw0KGgoAAAANSUhEUgAAA2gAAANoCAYAAAC1Fsk9AAAgAElEQVR4nOzdfbCfdX3n/yshRAkpKCogGAXFqEiwtgTbFczMTyvbRctqf/FXqC7dutyoW9d7WxVPraKj4l3qTbWi3R1BtumUu9URxcRaAZ2srR1LcYqCaKwaVsNZjMJg8v79AeeMx5ycfEKuc30/7+vzeM48Z+DLOddJXnN8vfM65iRdAAAAAACqoJv0DwAAAAAAcC8GGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQZaD9x8881x7rnnxpo1a2Lp0qWxbt26ed/uO9/5Tpx11lnxkIc8JB74wAfGCSecEJdffvmwP1gAwBx0OACgJgy0Hrjiiiti1apVsX79+li9evW8x33r1q1x9NFHxzOf+cy44oor4tprr413v/vdcemllw7/AwYAzKLDAQA1YaD1wM6dO2f/+Ywzzpj3uJ955plxyimnzHlbAMDk0eEAgJow0HpmvuN+xx13xIEHHugrrQBQOTocADBpDLSeme+4b9q0Kbqui8suuyxOOeWUWLZsWRx55JFxwQUXxM9//vPJ/EABALuhwwEAk8ZA65n5jvull14aXdfFIYccEq9+9atj06ZN8eY3vzmWLVsWF1544YLPm56ejq1bt856yy23xBe+8IW47bbb5rxOktm87bbbYsuWLXH33XcvYivvGzqcJPdujf09Jgy0npnvuF9yySXRdV2sX79+zut/9Ed/FIccckjs2rVrj8+bmpqKrutIcrRu2bJlMer4fqHDSbLcmvp7TBhoPTPfcf/0pz8dXdfFhz70oTmvX3755dF1XXznO9/Z4/N++auvX/7yl6PruljbPT1O6U4nyWrd21dgt2zZEl3XxW233bYYdXy/GKrDTz7+nDj1Sa/Yo08+44JenPTnwGK6UH774pN/54JenHQei5bzr76yF+U8XNa/9uwLejFbf48JA61n5jvut95667zH/W//9m+j67rYunVr8fO3bt0aXdfFKd3p8Ywl/y9JVmtpn+1LBy42Q3X4qU96RfzW2j/do08586JenPTnwGK6UH774lN+76JenHQei5bzyW/qRTkPl/Vv/H8X9WJJl9XU32PCQOuZPf0RzU984hPjd3/3d+e89pKXvCQe+tCH7tPzDTSSWSzts5oO/FAdbqDtvwbaQDkbaOmyNtDyY6D1wI4dO2Ljxo2xcePGWLt2bRx//PGz/75t27aIuPcrrUuWLImXv/zl8bnPfS6mpqbigAMOiPe///379LEMNJJZLO2zSR/4SXS4gbb/GmgD5WygpcvaQMuPgdYDM7/9ZT43b948+3aXXHJJPOEJT4jly5fHscceG+9+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etGL4sgjj4wHPOAB8djHPjb+4i/+Yva/33rrrdF13W6edtppQ/50qqUk5/nym/Hf/u3fZt/u7rvvjle96lVxxBFHxIoVK+IZz3hGfOMb3xj6p1QtfWY9339/3OMeN/RPqUpKcv75z38eb3/72+Nxj3tcHHTQQXHsscfGq1/96vjJT34y5+18Tt9/dPgw6PBh0N/DocPHh4E2Em644YZYunRpvOAFL4jPfOYz8a53vSsOOuigeOUrXznn7e6888444YQTYu3atfE//+f/jE2bNsUHP/jBeP/73z/7NjPH/e1vf3vccMMNs950001D/7SqozTnX8xtxsc+9rHxq7/6q3Pe7rzzzotDDz00Lr744vjMZz4Tp556ahx99NFxxx13DPnTqpK+s+66Ll72spfNebuvfe1rQ/6UqqQ05ze96U2xbNmyeNvb3habNm2KDRs2xMqVK+M//af/NOftfE7fP3T4MOjwYdDfw6HDx4mBNhJOO+20OPnkk+e8dtFFF8WBBx4YP/jBD2Zf+5M/+ZN4zGMeEz/96U/3+KyZ43755Zcv2o83K6U5/zIzmb7jHe+Yfe273/1uHHDAAfGRj3xk9rUf/ehHcfDBB8fb3/72/n/wyegz64h7D/x73vOeRfmxZqY058c97nFx9tlnz3m7N77xjXHQQQfNfqXW5/T9R4cPgw4fBv09HDp8nBhoI+Hwww+PN7zhDXNe+/rXvx5d18V//+//ffa1I444It761rcu+CzHfc+U5vzLXHjhhbFkyZL47ne/O/vaxRdfHEuWLInt27fPedvnPOc5sW7dul5/3BnpM+sIB35PlOb86Ec/Ol760pfOebt3vetd8YAHPCB27twZET6n9wcdPgw6fBj093Do8HFioI2EQw45JN785jfPee1f//Vfo+u6+OM//uOIiLjlllui67r40Ic+FKeffnosX748DjvssHjxi18856uxM8f9oQ99aCxdujQOP/zwePGLXxz/9//+30F/TjVSkvN8nHDCCbuV26tf/eo4+uijd3vb173udXHEEUf08uPNTJ9ZR9x74B/ykIfEAQccEA9+8IPj93//9+P73/9+3z/sdJTm/Na3vjUe/OAHx+c///m488474ytf+Uocc8wx8fKXv3z2bXxO3390+DDo8GHQ38Ohw8eJgTYSTjrppHj2s58957X/8T/+R3RdF+eee25ERFx//fXRdV2sXLky/vN//s9x7bXXxoYNG+Lggw+Oc845Z/b9/u3f/i1e/OIXx5VXXhmbN2+OCy+8MA4++OB42tOeFrt27Rr051UbJTn/Mv/0T/8UXdfFhz/84Tmv/5f/8l/iiU984m5v/853vjMOPPDA/n7QSekz64iIs88+OzZu3Bh/93d/Fxs2bIiHPexhsXr16tixY8ei/PizsC85X3DBBXO+Sf8FL3jB7FdeI3xO7w86fBh0+DDo7+HQ4ePEQBsJH//4x2PJkiXx53/+5/GjH/0o/v7v/z5WrVoVBxxwQJx33nkREXHddddF13W7/V7ld73rXbF06dLYtm3bHp9/6aWXRtd1ce211y7qz6N2SnL+ZV7zmtfEgQceGD/60Y/mvK4IF6bPrOdj5he7H/3oR/v+oaeiNOcNGzbEgx70oHjve98bf/d3fxcf/OAH4yEPecic3zLjc/r+o8OHQYcPg/4eDh0+Tgy0kbBr1654xSteEcuWLYuu62L58uXx1re+NR72sIfFn/7pn0ZExL/8y79E13Xx2te+ds77/uM//mN0XRdf/OIX9/j8n/3sZ3HAAQc0/02iJTn/8ts/8pGPjN/5nd/Z7b/5rQQL02fWe+Koo46KF73oRX3+sNNRkvP/+T//Jx7wgAfEBz7wgTnv+4lPfCKWLFkS3/zmNyPC5/T+oMOHQYcPg/4eDh0+Tgy0kXHHHXfEP/3TP8Udd9wR27Zti67r4jOf+UxE3Pt3Wyxfvny34/4P//AP0XVdfOlLX9rjc2eO+y//yUqtslDOv8gXv/jF6LouLrvsst3+28w34/7yH1373Oc+1zfj/gJ9ZL0njjrqqHjxi1/c5w83LQvl/JWvfCW6rovrrrtuzvvceOON0XVdfPazn40In9N9oMOHQYcPg/4eDh0+Lgy0EXPBBRfEscceO+cvKzz99NPjpJNOmvN2M//X9UK/reCSSy6Jruvi85///KL9eLMyX84znH/++bFy5cp5/0jsmT/O9hd/i8aPf/zjWLlyZfNf5d4T9zfr+fjSl74UXdfFxRdf3PcPMz2/nPMPfvCD6LouNmzYMOftZr7P4V//9V8jwud03+jwYdDhw6C/h0OH58dAGwnXX399vOMd74jPfvazceWVV8YLX/jCWL58+W7H+Ctf+UoceOCB8YIXvCCuueaaeM973hMrVqyIV73qVbNvMzU1FS972cvib/7mb+Laa6+NP/uzP4sVK1bEunXrmv8G89KcIyLuueeeeOhDHxrPf/7z9/i88847Lx70oAfFxz72sbjmmmti3bp1/kLI++gz6w9/+MPxwhe+MC677LLYtGlTvOc974mHPOQh8fjHP774FwRjpTTn//gf/2P8yq/8Slx00UWxadOm+PM///M47LDD4t//+38/5+18Tt8/dPgw6PBh0N/DocPHiYE2Ev73//7fcfLJJ8fBBx8cK1eujKc//elx/fXXz/u2n/3sZ+PXfu3XYvny5XHUUUfF6173urjnnntm//snP/nJOOmkk+LQQw+NZcuWxTHHHBOvetWr4ic/+clQP51q2Zec/9f/+l/RdV18+tOf3uPz7rrrrnjlK18Zhx9+eBx00EHxjGc8I2666abF+uGnos+sr7322vh3/+7fxWGHHRbLli2Lo446Ks4555wF/1CFVijNeXp6Ol71qlfFYx7zmHjgAx8Yxx57bPy3//bfdvv7cnxO3z90+DDo8GHQ38Ohw8eJgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCgAQAAAEAlGGgAAAAAUAkGGgAAAABUgoEGAAAAAJVgoAEAAABAJRhoAAAAAFAJBhoAAAAAVIKBBgAAAACVYKABAAAAQCUYaAAAAABQCQYaAAAAAFSCgQYAAAAAlWCg9cDNN98c5557bqxZsyaWLl0a69atm/ftvvOd78RZZ50VD3nIQ+KBD3xgnHDCCXH55ZcP+4MFAMxBhwMAasJA64ErrrgiVq1aFevXr4/Vq1fPe9y3bt0aRx99dDzzmc+MK664Iq699tp497vfHZdeeunwP2AAwCw6HABQEwZaD+zcuXP2n88444x5j/uZZ54Zp5xyypy3BQBMHh0OAKgJA61n5jvud9xxRxx44IG+0goAlaPDAQCTxkDrmfmO+6ZNm6LrurjsssvilFNOiWXLlsWRRx4ZF1xwQfz85z+fzA8UALAbOhwAMGkMtJ6Z77hfeuml0XVdHHLIIfHqV786Nm3aFG9+85tj2bJlceGFFy74vOnp6di6deust9xyS3zhC1+I2267bc7rJJnN2267LbZs2RJ33333IrbyvqHDSXLv1tjfY8JA65n5jvsll1wSXdfF+vXr57z+R3/0R3HIIYfErl279vi8qamp6LqOJEfrli1bFqOO7xc6nCTLram/x4SB1jPzHfdPf/rT0XVdfOhDH5rz+uWXXx5d18V3vvOdPT7vl7/6+uUvfzm6rou13dPjlO50kqzWvX0FdsuWLdF1Xdx2222LUcf3Cx1Okvearb/HhIHWM/Md91tvvXXe4/63f/u30XVdbN26tfj5W7duja7r4pTu9HjGkv+XJKu1tM/2pQMXGx1Okvda0mU19feYMNB6Zk9/RPMTn/jE+N3f/d05r73kJS+Jhz70ofv0fMedZBZL+6ymA6/DSfJeS7qspv4eEwZaD+zYsSM2btwYGzdujLVr18bxxx8/++/btm2LiHu/0rpkyZJ4+ctfHp/73OdiamoqDjjggHj/+9+/Tx/LcSeZxdI+m/SB1+EkubslXTbp/h4rBloPzPz2l/ncvHnz7Ntdcskl8YQnPCGWL18exx57bLz73e/e54/luJPMYmmfTfrA63CS3N2SLpt0f48VAy0ZjjvJLJb2WUsHXoeTzGJJl7XU30NioCXDcSeZxdI+a+nA63CSWSzpspb6e0gMtGSUHved3z+uFyddDhmUtZzHZl9Zl/ZZSwe+tMMfteGiXjxt9Wt6cdKfk4tpb1k/7rW9OOk8Wsl5zFkf876LerGky1rq7yEx0JJhoNWnrOU8Ng20xcNAq8/ahsOk82gl5zFnbaDlx0BLhoFWn7KW89g00BYPA60+axsOk86jlZzHnLWBlh8DLRkGWn3KWs5j00BbPAy0+qxtOEw6j1ZyHnPWBlp+DLRkGGj1KWs5j00DbfEw0OqztuEw6TxayXnMWRto+THQkmGg1aes5Tw2DbTFw0Crz9qGw6TzaCXnMWdtoOXHQEuGgVafspbz2DTQFg8DrT5rGw6TzqOVnMectYGWHwMtGQZafcpazmPTQFs8DLT6rG04TDqPVnIec9YGWn4MtGQYaPUpazmPTQNt8TDQ6rO24TDpPFrJecxZG2j5MdCSYaDVp6zlPDYNtMXDQKvP2obDpPNoJecxZ22g5cdAS4aBVp+ylvPYNNAWDwOtPmsbDpPOo5Wcx5y1gZYfAy0ZBlp9ylrOY9NAWzwMtPqsbThMOo9Wch5z1gZafgy0ZBho9SlrOY9NA23xMNDqs7bhMOk8Wsl5zFkbaPkx0JJhoNWnrOU8Ng20xcNAq8/ahsOk82gl5zFnbaDlx0BLhoFWRx88hQAAIABJREFUn7KW89g00BYPA60+axsOk86jlZzHnLWBlh8DLRkGWn3KWs5j00BbPAy0+qxtOEw6j1ZyHnPWBlp+DLRkGGj1KWs5j00DbfEw0OqztuEw6TxayXnMWRto+THQkmGg1aes5Tw2DbTFw0Crz9qGw6TzaCXnMWdtoOXHQEtG6XEnyUlb2mctHfjSDn/KmRf14m+t/dNenPTn0mL6lN+7qBd/6+Q39eKk82glZ1nv3ZIua6m/h8RAS4aBRjKLpX3W0oE30OqztuEw6TxayVnWBlrNGGjJMNBIZrG0z1o68AZafdY2HCadRys5y9pAqxkDLRkGGskslvZZSwfeQKvP2obDpPNoJWdZG2g1Y6D1wM033xznnnturFmzJpYuXRrr1q1b8O0vv/zy6LounvSkJ+3zxzLQSGaxtM8mfeBr7HADbThrGw6TzqOVnGVtoNWMgdYDV1xxRaxatSrWr18fq1evXvC4//SnP41jjjkmjjjiCAON5Kgt7bNJH/gaO9xAG87ahsOk82glZ1kbaDVjoPXAzp07Z//5jDPOWPC4X3DBBfG0pz0tzj77bAON5Kgt7bNJH/gaO9xAG87ahsOk82glZ1kbaDVjoPXMQsf9m9/8ZqxYsSK+9rWvGWgkR29pn9V04GvpcANtOGsbDpPOo5WcZW2g1YyB1jMLHffTTz89zj///IgIA43k6C3ts5oOfC0dbqANZ23DYdJ5tJKzrA20mjHQemZPx/2qq66KBz/4wXH77bdHRPlxn56ejq1bt866ZcsWA41kCvdGjQe+lg430IaztuEw6TxayVnWBlrNGGg9M99x/9nPfhaPfvSjY8OGDbOvlR73qamp6LpuNw00krW7N2o88LV0uIE2nLUNh0nn0UrOsjbQasZA65n5jvvb3va2OO644+L222+P7du3x/bt2+PMM8+ME044IbZv3x533333Hp/n/0EjmdW9UeOBr6XDDbThrG04TDqPVnKWtYFWMwZaz8x33M8+++x5v4I648c//vHi5/seNJJZLO2zmg58LR1uoA1nbcNh0nm0krOsDbSaMdB6Zr7jftNNN8XmzZvneNppp8VjHvOY2Lx5c3z/+98vfr6BRjKLpX1W04GvpcMNtOGsbThMOo9Wcpa1gVYzBloP7NixIzZu3BgbN26MtWvXxvHHHz/779u2bZv3ffwpjiTHbmmfTfrA19jhBtpw1jYcJp1HKznL2kCrGQOtB2699dY9/taXzZs3z/s+BhrJsVvaZ5M+8DV2uIE2nLUNh0nn0UrOsjbQasZAS4aBRjKLpX3W0oE30OqztuEw6TxayVnWBlrNGGjJMNBIZrG0z1o68AZafdY2HCadRys5y9pAqxkDLRkGGskslvZZSwdeh5PMYkmXtdTfQ2KgJcNxJ5nF0j5r6cDrcJJZLOmylvp7SAy0ZDjuJLNY2mctHXgdTjKLJV3WUn8PiYGWDMedZBZL+6ylA6/DSWaxpMta6u8hMdCS4biTzGJpn7V04HU4ySyWdFlL/T0kBloyHHeSWSzts5YOvA4nmcWSLmupv4fEQEuG404yi6V91tKB1+Eks1jSZS3195AYaMlw3ElmsbTPWjrwOpxkFku6rKX+HhIDLRmOO8kslvZZSwdeh5PMYkmXtdTfQ2KgJcNxJ5nF0j5r6cDrcJJZLOmylvp7SAy0ZDjuJLNY2mctHXgdTjKLJV3WUn8PiYGWDMedZBZL+6ylA6/DSWaxpMta6u8hMdCS4biTzGJpn7V04HU4ySyWdFlL/T0kBloyHHeSWSzts5YOvA4nmcWSLmupv4fEQEuG404yi6V91tKB1+Eks1jSZS3195AYaMlw3ElmsbTPWjrwOpxkFku6rKX+HhIDLRmOO8kslvZZSwdeh5PMYkmXtdTfQ2KgJcNxJ5nF0j5r6cDrcJJZLOmylvp7SAy0ZDjuJLNY2mctHXgdTjKLJV3WUn8PiYGWDMedZBZL+6ylA6/DSWaxpMta6u8hMdCS4biTzGJpn7V04HU4ySyWdFlL/T0kBloyHHeSWSzts5YOvA4nmcWSLmupv4fEQOuBm2++Oc4999xYs2ZNLF26NNatWzfnv09PT8fU1FSsXbs2Dj300Dj88MPj2c9+dvzzP//zPn8sx51kFkv7bNIHXoeT5O6WdNmk+3usGGg9cMUVV8SqVati/fr1sXr16t2O+9e//vU48sgj4/Wvf31cc801ceWVV8app54aBx98cHzjG9/Yp4/luJPMYmmfTfrA63CS3N2SLpt0f48VA60Hdu7cOfvPZ5xxxm7H/Sc/+Uns2LFjzmt33nlnHHbYYfGyl71snz6W404yi6V9NukDr8NJcndLumzS/T1WDLSeme+474mTTz45nve85+3T8x13klks7bOaDrwOJ8l7Lemymvp7TBhoPVN63Ldv3x4rVqyIqampfXq+404yi6V9VtOB1+Ekea8lXVZTf48JA61nSo/7OeecEytXrozvfe97C77d9PR0bN26ddYtW7Y47iRTuDdqPPA6nCTvdSFq7O8xYaD1TMlx/9jHPhZd18Vf/dVf7fV5U1NT0XXdbjruJGt3b9R44HU4Sd7rQtTY32PCQOuZvR33T3/607Fs2bK44IILip7nq68ks7o3ajzwOpwk73UhauzvMWGg9cxCx/2GG26IFStWxAtf+ML7/Xzfv0Ayi6V9VtOB1+Ekea8lXVZTf48JA61n9nTcb7zxxjjssMPiWc96Vtxzzz33+/mOO8kslvZZTQdeh5PkvZZ0WU39PSYMtB7YsWNHbNy4MTZu3Bhr166N448/fvbft23bFj/84Q/jEY94RBx99NHx+c9/Pm644YZZb7zxxn36WI47ySyW9tmkD7wOJ8ndLemySff3WDHQeuDWW2+d95vAu66LzZs3x+bNm/f430v/vp0ZHHeSWSzts0kfeB1Okrtb0mWT7u+xYqAlw3EnmcXSPmvpwOtwklks6bKW+ntIDLRkOO4ks1jaZy0deB1OMoslXdZSfw+JgZYMx51kFkv7rKUDr8NJZrGky1rq7yEx0JLhuJPMYmmftXTgdTjJLJZ0WUv9PSQGWjIcd5JZLO2zlg68DieZxZIua6m/h8RAS4bjTjKLpX3W0oHX4SSzWNJlLfX3kBhoyXDcSWaxtM9aOvA6nGQWS7qspf4eEgMtGY47ySyW9llLB16Hk8xiSZe11N9DYqAlw3EnmcXSPmvpwOtwklks6bKW+ntIDLRkOO4ks1jaZy0deB1OMoslXdZSfw+JgZYMx51kFkv7rKUDr8NJZrGky1rq7yEx0JLhuJPMYmmftXTgdTjJLJZ0WUv9PSQGWjIcd5JZLO2zlg68DieZxZIua6m/h8RAS4bjTjKLpX3W0oHX4SSzWNJlLfX3kBhoyXDcSWaxtM9aOvA6nGQWS7qspf4eEgMtGY47ySyW9llLB16Hk8xiSZe11N9DYqAlw3EnmcXSPmvpwOtwklks6bKW+ntIDLRkOO4ks1jaZy0deB1OMoslXdZSfw+JgZYMx51kFkv7rKUDr8NJZrGky1rq7yEx0JLhuJPMYmmftXTgdTjJLJZ0WUv9PSQGWjIcd5JZLO2zlg68DieZxZIua6m/h8RAS0bpcd/5/eN6cdLlkEFZy3ls9pV1aZ+1dOBLO/xRGy7qxdNWv6YXJ/05uZge876LelHWA+X8uNf25qQzqT3rki5rqb+HxEBLhoFWn7KW89g00BYPA60+DbRkORtog2Vd0mUt9feQGGjJMNDqU9ZyHpsG2uJhoNWngZYsZwNtsKxLuqyl/h4SA60Hbr755jj33HNjzZo1sXTp0li3bt28b/fRj340HvvYx8YDHvCAOPHEE+Pqq6/e549loNWnrOU8NlsbaDV2uIE2nAZaspwNtMGyLumySff3WDHQeuCKK66IVatWxfr162P16tXzHvdPfvKTsWTJknjDG94QmzZtivPOOy+WLVsWN9xwwz59LAOtPmUt57HZ2kCrscMNtOE00JLlbKANlnVJl026v8eKgdYDO3funP3nM844Y97jvnr16jjrrLPmvPabv/mb8du//dv79LEMtPqUtZzHZmsDrcYON9CG00BLlrOBNljWJV026f4eKwZaz8x33L/1rW9F13Vx5ZVXznn9fe97Xyxfvjzuuuuu4ucbaPUpazmPzdYG2i9SS4cbaMNpoCXL2UAbLOuSLqupv8eEgdYz8x33T33qU9F1Xdx8881zXv/sZz8bXdfFTTfdVPx8A60+ZS3nsWmgrZvz2iQ63EAbTgMtWc4G2mBZl3RZTf09Jgy0npnvuH/iE5+Iruvi9ttvn/P6li1bouu6uO666/b4vOnp6di6deusM+9joNWjrOU8Ng20dXNem0SHG2jDaaAly9lAGyzrhaixv8eEgdYzfR/3qamp6LpuNw20epS1nMemgbZuzmuT6HADbTgNtGQ5G2iDZb0QNfb3mDDQeqbv3x7j/0GrX1nLeWwaaOvmvDaJDjfQhtNAS5azgTZY1gtRY3+PCQOtZxb6BvOrrrpqzusbNmyI5cuXx9133138fN+DVp+ylvPYNNDWzXltEh1uoA2ngZYsZwNtsKxLuqym/h4TBlrPLPRHND//+c+f89pTn/pUf8z+CJS1nMemgbZut9eH7nADbTgNtGQ5G2iDZV3SZTX195gw0Hpgx44dsXHjxti4cWOsXbs2jj/++Nl/37ZtW0REXHrppbFkyZJ44xvfGJs3b47zzz8/li1bFtdff/0+fSwDrT5lLeex2dpAq7HDDbThNNCS5WygDZZ1SZdNur/HioHWA7feeuu83wTedV1s3rx59u0++tGPxnHHHRfLly+PNWvWxNVXX73PH8tAq09Zy3lstjbQauxwA204DbRkORtog2Vd0mWT7u+xYqAlw0CrT1nLeWy2NtCGxECrTwMtWc4G2mBZl3RZS/09JAZaMgy0+pS1nMemgbZ4GGj1aaAly9lAGyzrki5rqb+HxEBLhoFWn7KW89g00BYPA60+DbRkORtog2Vd0mUt9feQGGjJKD3uJDlpS/uspQNf2uEnn3VRL/7W2j/txUl/Li2mTznzol78rZPf1IuTzqOVnEed9e9d1IslXdZSfw+JgZYMA41kFkv7rKUDb6DVZ23DYdJ5tJLzqLM20NJjoCXDQCOZxdI+a+nAG2j1WdtwmHQereQ86qwNtPQYaMkw0EhmsbTPWjrwBlp91jYcJp1HKzmPOmsDLT0GWjIMNJJZLO2zlg68gVaftQ2HSefRSs6jztpAS4+BlgwDjWQWS/uspQNvoNVnbcNh0nm0kvOoszbQ0mOgJcNAI5nF0j5r6cAbaPVZ23CYdB6t5DzqrA209BhoyTDQSGaxtM9aOvAGWn3WNhwmnUcrOY86awMtPQZaMgw0klks7bOWDryBVp+1DYdJ59FKzqPO2kBLj4GWDAONZBZL+6ylA2+g1Wdtw2HSebSS86izNtDSY6Alw0AjmcXSPmvpwBto9VnbcJh0Hq3kPOqsDbT0GGjJMNBIZrG0z1o68AZafdY2HCadRys5jzprAy09BloyDDSSWSzts5YOvIFWn7UNh0nn0UrOo87aQEuPgZYMA41kFkv7rKUDb6DVZ23DYdJ5tJLzqLM20NJjoCXDQCOZxdI+a+nAG2j1WdtwmHQereQ86qwNtPQYaMkw0EhmsbTPWjrwBlp91jYcJp1HKzmPOmsDLT0GWjIMNJJZLO2zlg68gVaftQ2HSefRSs6jztpAS4+BlgwDjWQWS/uspQNvoNVnbcNh0nm0kvOoszbQ0mOgJcNAI5nF0j5r6cDrcJJZLOmylvp7SAy0ZDjuJLNY2mctHXgdTjKLJV3WUn8PiYGWDMedZBZL+6ylA6/DSWaxpMta6u8hMdAG5Morr4yTTz45Vq5cGUceeWQ873nPi1tuuWWfnuG4k8xiaZ9lOfA6nGRLlnRZlv7OhoE2EJs3b46lS5fGH/zBH8TnPve5uOyyy2L16tWxevXq+NnPflb8HMedZBZL+yzDgdfhJFuzpMsy9HdGDLSBOO+88+LYY4+NXbt2zb62adOm6Lourr/++uLnOO4ks1jaZxkOvA4n2ZolXZahvzNioA3EH/7hH8aJJ54457WvfvWr0XVdXHfddcXPcdxJZrG0zzIceB1OsjVLuixDf2fEQBuIL37xi7Fs2bL4wAc+EHfccUd861vfitNOOy1OOumk2LlzZ/FzHHeSWSztswwHXoeTbM2SLsvQ3xkx0AbkqquuipUrV0bXddF1XTz5yU+OH/7whwu+z/T0dGzdunXWLVu2OO4kU7g3sh14HU6yJRciW39nw0AbiC996UvxoAc9KF7xilfEpk2bYuPGjXHiiSfG2rVr46677trj+01NTc3+YuAXddxJ1u7eyHTgdTjJ1lyITP2dEQNtIH7913891q9fP+e17373u7FkyZK4+OKL9/h+vvpKMqt7I9OB1+EkW3MhMvV3Rgy0gTjooIPiwgsv3O31hz3sYfG6172u+Dm+f4FkFkv7LMOB1+EkW7OkyzL0d0YMtIF4/OMfH8997nPnvPbtb387lixZEh/5yEeKn+O4k8xiaZ9lOPA6nGRrlnRZhv7OiIE2EO9973uj67p46UtfOvuXnJ5wwgnx8Ic/PH784x8XP8dxJ5nF0j7LcOB1OMnWLOmyDP2dEQNtIHbt+v/bu9NYP8tq4cNPpw0dtIwtFhkqB9CAYwCNvtAP9IgRNcIH44TEYEXQOFFQUKyg9IQpeIoWUCvRqCCQGIIYIMyjpCoSAxhDQF6qErS0WxA1QNf7wbeNm9L2Lvz3vZ/1v68r+X3Z7O7Truyz7rN6aFkXF1xwQbzuda+LmTNnxi677BJHHHFE/O53v9uqr+Nxl5Sl0n2W4YG3wyW1Vskuy7C/M3KgJeNxl5Sl0n3W0gNvh0vKUskua2l/1+RAS8bjLilLpfuspQfeDpeUpZJd1tL+rsmBlozHXVKWSvdZSw+8HS4pSyW7rKX9XZMDLRmPu6Qsle6zlh54O1xSlkp2WUv7uyYHWjIed0lZKt1nLT3wdrikLJXsspb2d00OtGQ87pKyVLrPWnrg7XBJWSrZZS3t75ocaMl43CVlqXSftfTA2+GSslSyy1ra3zU50JLxuEvKUuk+a+mBt8MlZalkl7W0v2tyoCXjcZeUpdJ91tIDb4dLylLJLmtpf9fkQEvG4y4pS6X7rKUH3g6XlKWSXdbS/q7JgZaMx11Slkr3WUsPvB0uKUslu6yl/V2TAy0Zj7ukLJXus5YeeDtcUpZKdllL+7smB1oyHndJWSrdZy098Ha4pCyV7LKW9ndNDrRkPO6SslS6z1p64O1wSVkq2WUt7e+aHGjJeNwlZal0n7X0wNvhkrJUssta2t81OdCS8bhLylLpPmvpgbfDJWWpZJe1tL9rcqAl43GXlKXSfdbSA2+HS8pSyS5raX/X5EBLxuMuKUul+6ylB94Ol5Slkl3W0v6uyYGWjMddUpZK91lLD7wdLilLJbuspf1dkwMtGY+7pCyV7rOWHng7XFKWSnZZS/u7JgdaMh53SVkq3WctPfB2uKQsleyylvZ3TQ60ZDzukrJUus9aeuDtcElZKtllLe3vmhxoyXjcJWWpdJ+19MDb4ZKyVLLLWtrfNTnQkvG4S8pS6T5r6YG3wyVlqWSXtbS/a3KgVfTss8/GWWedFfvss0+MjIzEvHnz4thjj92qr+Fxl5Sl0n2W5YG3wyW1VMkuy7K/s3GgVXT00UfHK17xili+fHncfPPN8aMf/ShOOOGErfoaHndJWSrdZ1keeDtcUkuV7LIs+zsbB1ol1157bUydOjXuu+++l/R1PO6SslS6zzI88Ha4pNYq2WUZ9ndGDrRK3ve+98Xb3/72l/x1PO6SslS6zzI88Ha4pNYq2WUZ9ndGDrRKdt999/jUpz4Vn/70p+PlL395bLvttnH44YfHQw89tFVfx+MuKUul+yzDA2+HS2qtkl2WYX9n5ECrZGRkJGbNmhVvectb4uqrr47LLrss9tprr3jNa14TzzzzzCZ/3OjoaKxatWpDK1eu9LhLStGWZHrg7XBJrbU5mfZ3Rg60SqZNmxYzZsyIv/zlLxs+9qtf/Sq6rosrrrhikz9uyZIl0XXdRnncJfW9Lcn0wNvhklprczLt74wcaJXMmTMn3vzmN2/08dmzZ8fpp5++yR/nd18lZW1LMj3wdrik1tqcTPs7IwdaJQsWLNjk4/71r3+9+Ov48wuSslS6zzI88Ha4pNYq2WUZ9ndGDrRKzj777Jg+fXo8/vjjGz62/ndSr7rqquKv43GXlKXSfZbhgbfDJbVWyS7LsL8zcqBVMjo6GrvvvnscdNBBceWVV8Yll1wS8+fPjwMPPDDWrVtX/HU87pKyVLrPMjzwdrik1irZZRn2d0YOtIoefPDBeOc73xkzZ86M2bNnxwc+8IF47LHHtupreNwlZal0n2V54O1wSS1Vssuy7O9sHGjJeNwlZal0n7X0wNvhkrJUssta2t81OdCS8bhLylLpPmvpgbfDJWWpZJe1tL9rcqAl43GXlKXSfdbSA2+HS8pSyS5raX/X5EBLpvRxf+7P/zWQJno5ZMiszXnYGtSsS/dZSw986Q7f83/PGUiH7XPSQJro78nxzKzbnPNQz/ob5wykkl3W0v6uyYGWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMk40PqXWZvzsOVAGz8OtP5l1m3Oeahn7UBLz4GWjAOtf5m1OQ9bDrTx40DrX2bd5pyHetYOtPQcaMmUPu6SNNGV7rOWHvjSHX7QB88ZSP990GkDaaK/l8Yzs67Tmz9wzkBa+ObTBlcP5tLnWZfsspb2d00OtGQcaJKyVLrPWnrgHWj9y6zr5EDLN+uSXdbS/q7JgZaMA01Slkr3WUsPvAOtf5l1nRxo+WZdssta2t81OdCScaBJylLpPmvpgXeg9S+zrpMDLd+sS3ZZS/u7JgfaBHnyySdj1113ja7r4p577in+cQ40SVkq3WcZH/jx3uGOhnqZdZ0caPlmXbLLMu7vDBxoE+Skk06KuXPnOtAkDW2l+yzjAz/eO9zRUC+zrpMDLd+sS3ZZxv2dgQNtAjzwwAMxc+bMuPDCCx1okoa20n2W7YGvscMdDfUy6zo50PLNumSXZdvfWTjQJsDChQvjhBNOiJtuusmBJmloK91n2R74Gjvc0VAvs66TAy3frEt2Wbb9nYUDrbLLL7885s6dG6Ojow40SUNd6T7L9MDX2uGOhnqZdZ0caPlmXbLLMu3vTBxoFf3973+P3XbbLVasWBERUfS4j46OxqpVqza0cuVKB5qkFG1Jtge+5g53NNTLrOvkQMs3683Jtr+zcaBVdPLJJ8cBBxwQ69ati4iyx33JkiXRdd1GOdAk9b0tyfbA19zhjoZ6mXWdHGj5Zr052fZ3Ng60Sv7whz/EyMhIXH311bFmzZpYs2ZNXHXVVdF1Xdx6663x5JNPvuCP8/9Bk5S1Lcn0wNfe4Y6Gepl1nRxo+Wa9OZn2d0YOtErW/07rplqwYEHR1/Fn0CRlqXSfZXjga+9wR0O9zLpODrR8sy7ZZRn2d0YOtErWrFkTN91005jOO++86LouvvOd7xT/IXMHmqQsle6zDA987R3uaKiXWdfJgZZv1iW7LMP+zsiBNoH8LY6ShrnSfZb1gfe3OA5HZl0nB1q+WZfssqz7u+8caBPIgSZpmCvdZ1kfeAfacGTWdXKg5Zt1yS7Lur/7zoGWjANNUpZK91lLD7wDrX+ZdZ0caPlmXbLLWtrfNTnQknGgScpS6T5r6YF3oPUvs66TAy3frEt2WUv7uyYHWjIONElZKt1nLT3wDrT+ZdZ1cqDlm3XJLmtpf9fkQEvGgSYpS6X7rKUH3g6XlKWSXdbS/q7JgZaMx11Slkr3WUsPvB0uKUslu6yl/V2TAy0Zj7ukLJXus5YeeDtcUpZKdllL+7smB1oyHndJWSrdZy098Ha4pCyV7LKW9ndNDrRkPO6SslS6z1p64O1wSVkq2WUt7e+aHGjJeNwlZal0n7X0wNvhkrJUssta2t81OdCS8bhLylLpPmvpgbfDJWWpZJe1tL9rcqAl43GXlKXSfdbSA2+HS8pSyS5raX/X5EBLxuMuKUul+6ylB94Ol5Slkl3W0v6uyYGWjMddUpZK91lLD7wdLilLJbuspf1dkwMtGY+7pCyV7rOWHng7XFKWSnZZS/u7JgdaMh53SVkq3WctPfB2uKQsleyylvZ3TQ60ZDzukrJUus9aeuDtcElZKtllLe3vmhxoyXjcJWWpdJ+19MDb4ZKyVLLLWtrfNTnQkvG4S8pS6T5r6YG3wyVlqWSXtbS/a3KgJeNxl5Sl0n3W0gNvh0vKUskua2l/1+RAS8bjLilLpfuspQfeDpeUpZJd1tL+rskrKXtVAAASm0lEQVSBlozHXVKWSvdZSw+8HS4pSyW7rKX9XZMDLRmPu6Qsle6zlh54O1xSlkp2WUv7uyYHWjIed0lZKt1nLT3wdrikLJXsspb2d00OtGQ87pKyVLrPWnrg7XBJWSrZZS3t75ocaJVcdtll8Z73vCd23XXXmDFjRrz+9a+Piy++ONatW7dVX8fjLilLpfsswwNvh0tqrZJdlmF/Z+RAq+Qtb3lLvP/9749LL700brjhhvjiF78YkydPjq9//etb9XU87pKyVLrPMjzwdrik1irZZRn2d0YOtEr+8pe/bPSxRYsWxfbbb79VX8fjLilLpfsswwNvh0tqrZJdlmF/Z+RAm0DLly+Pruvi6aefLv4xHndJWSrdZ1kfeDtc0jBXssuy7u++c6BNoA9+8IOxxx57bNWP8bhLylLpPsv6wNvhkoa5kl2WdX/3nQNtgtx2220xefLkWLZs2WY/b3R0NFatWrWhlStXetwlpWhLMj/wdrikYW9zMu/vDBxoE+DRRx+NefPmxaGHHhrPPffcZj93yZIl0XXdRnncJfW9Lcn6wNvhklpoc7Lu7ywcaJWtWbMm9t9//3jta18ba9eu3eLn+91XSVnbkowPvB0uqZU2J+P+zsSBVtHTTz8db3vb22K33XZ70d/Q/vyCpCyV7rMsD7wdLqmlSnZZlv2djQOtkmeeeSbe9a53xQ477BD33Xffi/46HndJWSrdZxkeeDtcUmuV7LIM+zsjB1olixYtiq7r4txzz4277rprTP/85z+Lv47HXVKWSvdZhgfeDpfUWiW7LMP+zsiBVskee+zxgn9QvOu6ePjhh4u/jsddUpZK91mGB94Ol9RaJbssw/7OyIGWjMddUpZK91lLD7wdLilLJbuspf1dkwMtGY+7pCyV7rOWHng7XFKWSnZZS/u7JgdaMh53SVkq3WctPfB2uKQsleyylvZ3TQ60ZDzukrJUus9aeuDtcElZKtllLe3vmhxoyXjcJWWpdJ+19MDb4ZKyVLLLWtrfNTnQkvG4S8pS6T5r6YG3wyVlqWSXtbS/a3KgJeNxl5Sl0n3W0gNvh0vKUskua2l/1+RAS8bjLilLpfuspQfeDpeUpZJd1tL+rsmBlozHXVKWSvdZSw+8HS4pSyW7rKX9XZMDLRmPu6Qsle6zlh54O1xSlkp2WUv7uyYHWjIed0lZKt1nLT3wdrikLJXsspb2d00OtGQ87pKyVLrPWnrg7XBJWSrZZS3t75ocaMl43CVlqXSftfTA2+GSslSyy1ra3zU50JLxuEvKUuk+a+mBt8MlZalkl7W0v2tyoCXjcZeUpdJ91tIDb4dLylLJLmtpf9fkQEvG4y4pS6X7rKUH3g6XlKWSXdbS/q7JgZaMx11Slkr3WUsPvB0uKUslu6yl/V2TAy0Zj7ukLJXus5YeeDtcUpZKdllL+7smB1oyHndJWSrdZy098Ha4pCyV7LKW9ndNDrRkPO6SslS6z1p64O1wSVkq2WUt7e+aHGjJeNwlZal0n7X0wNvhkrJUssta2t81OdCSKX3cn/vzfw2kiV4OGTJrcx62BjXr0n3W0gNfusP3/N9zBtJhe584kCb6e3I82/Mb5wykw/Y5aSBN9DxambNZb7mSXdbS/q7JgZaMA61/mbU5D1sOtPHjQOtffTscJnoerczZrB1ofeZAS8aB1r/M2pyHLQfa+HGg9a++HQ4TPY9W5mzWDrQ+c6BV9MADD8TChQtjxowZMXfu3DjxxBPjX//611Z9DQda/zJrcx62HGgvrOYOd6DVq2+Hw0TPo5U5m7UDrc8caJU88cQT8YpXvCIOOeSQuOaaa2LFihUxe/bs+OQnP7lVX8eB1r/M2pyHLQfaxmrvcAdavfp2OEz0PFqZs1k70PrMgVbJ0qVLY9asWbF69eoNH7voootiypQp8cc//rH46zjQ+pdZm/Ow5UDbWO0d7kCrV98Oh4meRytzNmsHWp850Co5+OCD44gjjhjzsTVr1sSkSZPi4osvLv46DrT+ZdbmPGw50DZWe4c70OrVt8NhoufRypzN2oHWZw60Snbeeef40pe+tNHH582bF1/4wheKv44DrX+ZtTkPWw60jdXe4Q60evXtcJjoebQyZ7N2oPWZA62SqVOnxtlnn73Rx/fbb79YtGjRJn/c6OhorFq1akO/+MUvouu6OLA7NP5Pd/gm+7+/3nMgbe5/hszanIezQc36P3fXC7Vy5croui4eeeSR8Vy/A1F7h7/ytFMH0oL5nxhIE/09OZ698qunDqQF848bSBM9j1bmbNZbblj2d0YOtEpe7OO+ZMmS6LpOkoa2lStXjuf6HQg7XJI2LsP+zsiBVsmL/ddjnv+7rw899FDcfPPN8cgjj2zxd6dLfudj5cqVL+nryKz7kjnnm/MjjzwSK1eu3Oq/qn4i2OHtZtbmPEwNas6Z9ndGDrRKDj744DjyyCPHfGzt2rVb/QfMB2XVKv/ucC1mXYc519HqnO3wdpl1HeZchznn4ECrZOnSpfGyl70s1qxZs+Fj3/nOd7b6r2geFP8LWo9Z12HOdbQ6Zzu8XWZdhznXYc45ONAqWf8fOV2wYEFce+218b3vfS+22267rf6PnA6K/wWtx6zrMOc6Wp2zHd4us67DnOsw5xwcaBXdf//9ceihh8b06dNjzpw5sXjx4gn7d3dHR0djyZIlMTo6OiH/81ti1nWYcx0tz9kOb5NZ12HOdZhzDg40AACAnnCgAQAA9IQDDQAAoCccaAAAAD3hQAMAAOgJB9oQueqqq+KNb3xjjIyMxCtf+cr4yle+Es8+++xGn7d69eo47rjjYpdddoltttkm9t5777jwwgs3/POHH344uq7bqMMOO6zmL6e3Sub8QvNb35/+9KcNn/evf/0rFi9eHHPnzo0ZM2bEwoUL43e/+13tX1JvDXLWL/TP991339q/pF4qmfOzzz4bZ555Zuy7774xffr0mD9/fpx44onx1FNPjfk839Mvnh1ehx1eh/1djx0+fBxoQ+Kuu+6KyZMnx1FHHRXXXHNNnHvuuTF9+vQ44YQTxnzek08+Gfvvv38ceOCB8ZOf/CRuvPHGWL58eXzzm9/c8DnrH/czzzwz7rrrrg098MADtX9ZvVM65/+c2/r23nvveMMb3jDm84499tiYPXt2rFixIq655po4+OCDY9ddd421a9fW/GX10qBn3XVdfPaznx3zeb/5zW9q/pJ6qXTOp512WkydOjX+53/+J2688cZYtmxZzJo1Kz7ykY+M+Tzf0y+OHV6HHV6H/V2PHT6cHGhD4rDDDouDDjpozMfOOeecmDZtWjz22GMbPnbyySfHXnvtFU8//fQmv9b6x/2nP/3puP18syqd8/Otn+lZZ5214WOPPvpoTJkyJb797W9v+Njq1atj5syZceaZZw7+J5/MIGcd8e8H/rzzzhuXn2tmpXPed9994+ijjx7zeV/5yldi+vTpG36n1vf0i2eH12GH12F/12OHDycH2pCYM2dOfPnLXx7zsd/+9rfRdV18//vf3/CxuXPnxtKlSzf7tTzum1Y65+c744wzYtKkSfHoo49u+NiKFSti0qRJsWbNmjGfe8QRR8SCBQsG+vPOaJCzjvDAb0rpnF/1qlfFpz/96TGfd+6558Y222wTzz33XET4nn4p7PA67PA67O967PDh5EAbEi9/+cvja1/72piP/f73v4+u6+KLX/xiREQ89NBD0XVdXHDBBXH44YfHyMhI7LDDDnH88ceP+d3Y9Y/7TjvtFJMnT445c+bE8ccfH3/729+q/pr6qGTOL2T//fffaLmdeOKJseuuu270uaecckrMnTt3ID/fzAY564h/P/A77rhjTJkyJbbffvv40Ic+FH/+858H/dNOp3TOS5cuje233z5uuOGGePLJJ+Puu++OPffcMz73uc9t+Bzf0y+eHV6HHV6H/V2PHT6cHGhD4oADDoh3v/vdYz72gx/8ILqui49//OMREXHnnXdG13Uxa9as+OhHPxrXX399LFu2LGbOnBmLFi3a8OP+9Kc/xfHHHx9XXnll3HTTTXHGGWfEzJkz45BDDol169ZV/XX1Tcmcn+/ee++NruvioosuGvPxj33sY7Hffvtt9Plnn312TJs2bXA/6aQGOeuIiKOPPjouv/zyuOWWW2LZsmWx8847xz777BN///vfx+Xnn8XWzPnUU08d84f0jzrqqA2/8xrhe/qlsMPrsMPrsL/rscOHkwNtSFx88cUxadKkOP/882P16tVx2223xW677RZTpkyJY489NiIi7rjjjui6bqN/V/ncc8+NyZMnx+OPP77Jr//jH/84uq6L66+/flx/HX1XMufnO+mkk2LatGmxevXqMR+3CDdvkLN+Iev/j93vfve7g/6pp1I652XLlsV2220X3/jGN+KWW26J5cuXx4477jjmX5nxPf3i2eF12OF12N/12OHDyYE2JNatWxef//znY+rUqdF1XYyMjMTSpUtj5513jq9+9asREXH//fdH13XxhS98YcyPveeee6Lrurj11ls3+fX/8Y9/xJQpU5r/Q6Ilc37+5+++++7xnve8Z6N/5l8l2LxBznpT5s2bF8cdd9wgf9rplMz5r3/9a2yzzTbxrW99a8yP/eEPfxiTJk2KBx98MCJ8T78Udngddngd9nc9dvhwcqANmbVr18a9994ba9eujccffzy6rotrrrkmIv7937YYGRnZ6HH/9a9/HV3Xxe23377Jr7v+cX/+36zUqs3N+T/deuut0XVdXHrppRv9s/V/GPf5f3XtkUce6Q/j/odBzHpT5s2bF8cff/wgf7ppbW7Od999d3RdF3fccceYH3PfffdF13Vx3XXXRYTv6UGww+uww+uwv+uxw4eLA22InXrqqTF//vwx/7HCww8/PA444IAxn7f+/3W9uX+t4Ec/+lF0XRc33HDDuP18s3qhOa/3iU98ImbNmvWCfyX2+r/O9j//FY0nnngiZs2a1fzvcm/Ki531C7n99tuj67pYsWLFoH+a6T1/zo899lh0XRfLli0b83nr/5zD73//+4jwPT1odngddngd9nc9dnh+DrQhceedd8ZZZ50V1113XVx55ZVxzDHHxMjIyEaP8d133x3Tpk2Lo446Kq699to477zzYsaMGbF48eINn7NkyZL47Gc/G1dccUVcf/31cfrpp8eMGTNiwYIFzf8B89I5R0Q888wzsdNOO8WHP/zhTX69Y489Nrbbbrv43ve+F9dee20sWLDAfxDy/xvkrC+66KI45phj4tJLL40bb7wxzjvvvNhxxx3j1a9+dfH/QTCsSuf83ve+N172spfFOeecEzfeeGOcf/75scMOO8Q73vGOMZ/ne/rFscPrsMPrsL/rscOHkwNtSPzyl7+Mgw46KGbOnBmzZs2KQw89NO68884X/Nzrrrsu3vSmN8XIyEjMmzcvTjnllHjmmWc2/PNLLrkkDjjggJg9e3ZMnTo19txzz1i8eHE89dRTtX45vbU1c/7Zz34WXdfFz3/+801+vX/+859xwgknxJw5c2L69OmxcOHCeOCBB8brp5/KIGd9/fXXx1vf+tbYYYcdYurUqTFv3rxYtGjRZv9ShVaUznl0dDQWL14ce+21V2y77bYxf/78+MxnPrPRfy/H9/SLY4fXYYfXYX/XY4cPJwcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABATzjQAAAAesKBBgAA0BMONAAAgJ5woAEAAPSEAw0AAKAnHGgAAAA94UADAADoCQcaAABAT/w/UzI6uDAnbzQAAAAASUVORK5CYII=\" width=\"799.3333095113444\">" ], "text/plain": [ "<IPython.core.display.HTML object>" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "<matplotlib.image.AxesImage at 0x7f71963acf98>" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dummy_image = numpy.zeros(mask.shape, dtype=\"float32\")\n", "dummy_image[::5,::5] = 1\n", "#dummy_image[mask] = -1\n", "csr = csr_matrix(pre_csr)\n", "dummy_blurred = csr.T.dot(dummy_image.ravel()).reshape(mask.shape)\n", "fix, ax = subplots(2,2, figsize=(8,8))\n", "ax[0,0].imshow(dummy_image)\n", "ax[0,1].imshow(csr.dot(dummy_image.ravel()).reshape(mask.shape))\n", "ax[1,1].imshow(csr.T.dot(dummy_image.ravel()).reshape(mask.shape))\n" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0, 16)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ax[0,0].set_xlim(964,981)\n", "ax[0,0].set_ylim(0,16)\n", "ax[0,1].set_xlim(964,981)\n", "ax[0,1].set_ylim(0,16)\n", "ax[1,1].set_xlim(964,981)\n", "ax[1,1].set_ylim(0,16)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 672 ms, sys: 28 ms, total: 700 ms\n", "Wall time: 699 ms\n", "(1, 31, 0.00036844599621312167, 4.0083979131992555e-05, 2.1620353817016356, 4.873728452382846, 195.49168529213352)\n" ] } ], "source": [ "blured = csr.T.dot(dummy_image.ravel())\n", "\n", "# Invert this matrix: see https://arxiv.org/abs/1006.0758\n", "\n", "%time res = linalg.lsmr(csr.T, blured)\n", "\n", "restored = res[0].reshape(mask.shape)\n", "ax[1,0].imshow(restored)\n", "ax[1,0].set_xlim(964,981)\n", "ax[1,0].set_ylim(0,16)\n", "\n", "print(res[1:])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Conclusion of the raytracing part:\n", "\n", "We are able to simulate the path and the absorption of the photon in the thickness of the detector. \n", "Numba helped substentially to make the raytracing calculation much faster. \n", "The signal of each pixel is indeed spread on the neighboors, depending on the position of the PONI and this effect can be inverted using sparse-matrix pseudo-inversion.\n", "\n", "We will now save this sparse matrix to file in order to be able to re-use it in next notebook. But before saving it, it makes sense to spend some time in generating a high quality sparse matrix in throwing thousand rays per pixel in a grid of 32x32." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 5min 33s, sys: 1.68 s, total: 5min 34s\n", "Wall time: 5min 32s\n" ] } ], "source": [ "%time pre_csr = thick.calc_csr(32)\n", "hq_csr = csr_matrix(pre_csr)\n", "from scipy.sparse import save_npz\n", "save_npz(\"csr.npz\",hq_csr)" ] } ], "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.5.3" } }, "nbformat": 4, "nbformat_minor": 2 }