{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# The Helmert transform\n", "$\n", "\\begin{bmatrix}\n", " x\\\\\n", " y\\\\\n", " z\\\\\n", "\\end{bmatrix}^B\n", "=\n", "\\begin{bmatrix}\n", " t_X\\\\\n", " t_Y\\\\\n", " t_Z\\\\\n", "\\end{bmatrix}\n", "+\n", "\\begin{bmatrix}\n", " 1+s & -r_Z & r_Y\\\\\n", " r_Z & 1+s & -r_X\\\\\n", " -r_Y & r_X & 1+s\\\\\n", "\\end{bmatrix}\n", "\\cdot\n", "\\begin{bmatrix}\n", " x\\\\\n", " y\\\\\n", " z\\\\\n", "\\end{bmatrix}^A\n", "$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Using an external Rust library to speed up lon, lat to [BNG](https://en.wikipedia.org/wiki/Ordnance_Survey_National_Grid) conversion" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# -*- coding: utf-8 -*-" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import matplotlib as mpl\n", "mpl.use('TkAgg')\n", "import matplotlib.pyplot as plt\n", "import math\n", "from ctypes import cdll, c_float, c_double, Structure, ARRAY, POINTER, c_int32, c_uint32, c_size_t, c_void_p, cast\n", "from sys import platform\n", "from bng import bng\n", "import pyproj\n", "import ipdb\n", "from array import array\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def helmert(x, y, z=0):\n", " \"\"\" Example implementation of Helmert transform \"\"\"\n", " tX = -446.448 \n", " tY = 125.157 \n", " tZ = -542.060 \n", "\n", " rX = -0.1502 \n", " rY = -0.2470 \n", " rZ = -0.8421 \n", "\n", " s = 20.4894 * math.pow(10, -6)\n", " # For multiple x and y\n", " # A_vector = np.matrix([[x, y, z], [x, y, z]]).T\n", " A_vector = np.vstack(np.array([x, y, z]))\n", " t_matrix = np.vstack(np.array([tX, tY, tZ]))\n", " conversion = np.matrix([[1 + s, -rZ, rY], [rZ, 1 + s, -rX], [-rY, rX, 1 + s]])\n", " return t_matrix + (conversion * A_vector)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "matrix([[-402.49686677],\n", " [ 181.4468293 ],\n", " [-550.75780346]])" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helmert(-2.018304, 54.589097)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Setting up the Rust library. See [here](https://github.com/alexcrichton/rust-ffi-examples/tree/master/python-to-rust) for more" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ensure you've built your Rust library using `cargo build --release`, or the next step will fail.\n", "\n", "The boilerplate below can easily be hidden in a wrapper function – it's just here to demonstrate how to call into a shared Rust lib using FFI." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "if platform == \"darwin\":\n", " ext = \"dylib\"\n", "else:\n", " ext = \"so\"\n", " \n", "lib = cdll.LoadLibrary('target/release/liblonlat_bng.' + ext)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define the `ctypes` structures for lon, lat --> BNG conversion" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class _FFIArray(Structure):\n", " \"\"\" Convert sequence of floats to a C-compatible void array \"\"\"\n", " _fields_ = [(\"data\", c_void_p),\n", " (\"len\", c_size_t)]\n", "\n", " @classmethod\n", " def from_param(cls, seq):\n", " \"\"\" Allow implicit conversions from a sequence of 64-bit floats.\"\"\"\n", " return seq if isinstance(seq, cls) else cls(seq)\n", "\n", " def __init__(self, seq, data_type = c_double):\n", " \"\"\"\n", " Convert sequence of values into array, then ctypes Structure\n", "\n", " Rather than checking types (bad), we just try to blam seq\n", " into a ctypes object using from_buffer. If that doesn't work,\n", " we try successively more conservative approaches:\n", " numpy array -> array.array -> read-only buffer -> CPython iterable\n", " \"\"\"\n", " if isinstance(seq, float):\n", " seq = array('d', [seq])\n", " try:\n", " len(seq)\n", " except TypeError:\n", " # we've got an iterator or a generator, so consume it\n", " seq = array('d', seq)\n", " array_type = data_type * len(seq)\n", " try:\n", " raw_seq = array_type.from_buffer(seq.astype(np.float64))\n", " except (TypeError, AttributeError):\n", " try:\n", " raw_seq = array_type.from_buffer_copy(seq.astype(np.float64))\n", " except (TypeError, AttributeError):\n", " # it's a list or a tuple\n", " raw_seq = array_type.from_buffer(array('d', seq))\n", " self.data = cast(raw_seq, c_void_p)\n", " self.len = len(seq)\n", " \n", "\n", "class _Result_Tuple(Structure):\n", " \"\"\" Container for returned FFI data \"\"\"\n", " _fields_ = [(\"e\", _FFIArray),\n", " (\"n\", _FFIArray)]\n", "\n", "\n", "def _void_array_to_list(restuple, _func, _args):\n", " \"\"\" Convert the FFI result to Python data structures \"\"\"\n", " eastings = POINTER(c_double * restuple.e.len).from_buffer_copy(restuple.e)[0]\n", " northings = POINTER(c_double * restuple.n.len).from_buffer_copy(restuple.n)[0]\n", " res_list = [list(eastings), list(northings)]\n", " drop_array(restuple.e, restuple.n)\n", " return res_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define `ctypes` input and return parameters" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Multi-threaded FFI functions\n", "convert_bng = lib.convert_to_bng_threaded\n", "convert_bng.argtypes = (_FFIArray, _FFIArray)\n", "convert_bng.restype = _Result_Tuple\n", "convert_bng.errcheck = _void_array_to_list\n", "convert_bng.__doc__ = \"\"\"\n", " Multi-threaded lon, lat --> BNG conversion\n", " Returns a list of two lists containing Easting and Northing floats,\n", " respectively\n", " Uses the Helmert transform\n", " \"\"\"\n", "\n", "convert_lonlat = lib.convert_to_lonlat_threaded\n", "convert_lonlat.argtypes = (_FFIArray, _FFIArray)\n", "convert_lonlat.restype = _Result_Tuple\n", "convert_lonlat.errcheck = _void_array_to_list\n", "convert_lonlat.__doc__ = \"\"\"\n", " Multi-threaded BNG --> lon, lat conversion\n", " Returns a list of two lists containing Longitude and Latitude floats,\n", " respectively\n", " Uses the Helmert transform\n", " \"\"\"\n", "\n", "convert_to_osgb36 = lib.convert_to_osgb36_threaded\n", "convert_to_osgb36.argtypes = (_FFIArray, _FFIArray)\n", "convert_to_osgb36.restype = _Result_Tuple\n", "convert_to_osgb36.errcheck = _void_array_to_list\n", "convert_to_osgb36.__doc__ = \"\"\"\n", " Multi-threaded lon, lat --> OSGB36 conversion, using OSTN02 data\n", " Returns a list of two lists containing Easting and Northing floats,\n", " respectively\n", " \"\"\"\n", "\n", "convert_osgb36_to_lonlat = lib.convert_osgb36_to_ll_threaded\n", "convert_osgb36_to_lonlat.argtypes = (_FFIArray, _FFIArray)\n", "convert_osgb36_to_lonlat.restype = _Result_Tuple\n", "convert_osgb36_to_lonlat.errcheck = _void_array_to_list\n", "convert_osgb36_to_lonlat.__doc__ = \"\"\"\n", " Multi-threaded OSGB36 --> Lon, Lat conversion, using OSTN02 data\n", " Returns a list of two lists containing Easting and Northing floats,\n", " respectively\n", " \"\"\"\n", "\n", "convert_etrs89_to_lonlat = lib.convert_etrs89_to_ll_threaded\n", "convert_etrs89_to_lonlat.argtypes = (_FFIArray, _FFIArray)\n", "convert_etrs89_to_lonlat.restype = _Result_Tuple\n", "convert_etrs89_to_lonlat.errcheck = _void_array_to_list\n", "convert_etrs89_to_lonlat.__doc__ = \"\"\"\n", " Multi-threaded ETRS89 Eastings and Northings --> OSGB36 conversion, using OSTN02 data\n", " Returns a list of two lists containing Easting and Northing floats,\n", " respectively\n", " \"\"\"\n", "\n", "convert_etrs89_to_osgb36 = lib.convert_etrs89_to_osgb36_threaded\n", "convert_etrs89_to_osgb36.argtypes = (_FFIArray, _FFIArray)\n", "convert_etrs89_to_osgb36.restype = _Result_Tuple\n", "convert_etrs89_to_osgb36.errcheck = _void_array_to_list\n", "convert_etrs89_to_osgb36.__doc__ = \"\"\"\n", " Multi-threaded OSGB36 Eastings and Northings --> ETRS89 Eastings and Northings conversion,\n", " using OSTN02 data\n", " Returns a list of two lists containing Easting and Northing floats,\n", " respectively\n", " \"\"\"\n", "\n", "convert_osgb36_to_etrs89 = lib.convert_osgb36_to_etrs89_threaded\n", "convert_osgb36_to_etrs89.argtypes = (_FFIArray, _FFIArray)\n", "convert_osgb36_to_etrs89.restype = _Result_Tuple\n", "convert_osgb36_to_etrs89.errcheck = _void_array_to_list\n", "convert_osgb36_to_etrs89.__doc__ = \"\"\"\n", " Multi-threaded ETRS89 Eastings and Northings --> Lon, Lat conversion,\n", " Returns a list of two lists containing Longitude and Latitude floats,\n", " respectively\n", " \"\"\"\n", "\n", "convert_epsg3857_to_wgs84 = lib.convert_epsg3857_to_wgs84_threaded\n", "convert_epsg3857_to_wgs84.argtypes = (_FFIArray, _FFIArray)\n", "convert_epsg3857_to_wgs84.restype = _Result_Tuple\n", "convert_epsg3857_to_wgs84.errcheck = _void_array_to_list\n", "convert_epsg3857_to_wgs84.__doc__ = \"\"\"\n", " Convert Google Web Mercator (EPSG3857) coordinates to WGS84\n", " Latitude and Longitude\n", " Returns a list of two lists containing latitudes and longitudes,\n", " respectively\n", " \"\"\"\n", "\n", "# Free FFI-allocated memory\n", "drop_array = lib.drop_float_array\n", "drop_array.argtypes = (_FFIArray, _FFIArray)\n", "drop_array.restype = None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Simple test of average conversion speed, Python version" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test: 1MM random points within the UK" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# UK bounding box\n", "N = 55.811741\n", "E = 1.768960\n", "S = 49.871159\n", "W = -6.379880\n", "\n", "bng = pyproj.Proj(init='epsg:27700')\n", "bng_ostn02 = pyproj.Proj(\"+init=EPSG:27700 +nadgrids=OSTN02_NTv2.gsb\")\n", "wgs84 = pyproj.Proj(init='epsg:4326')\n", "\n", "num_coords = 1000000\n", "lon_ls = list(np.random.uniform(W, E, [num_coords]))\n", "lat_ls = list(np.random.uniform(S, N, [num_coords]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pure Python" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%%timeit -r50\n", "[bng(lat, lon) for lat, lon in zip(lat_ls, lon_ls)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pyproj" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 loop, best of 50: 599 ms per loop\n" ] } ], "source": [ "%%timeit -r50\n", "pyproj.transform(wgs84, bng, lon_ls, lat_ls)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Multithreaded Rust" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 loop, best of 50: 1.16 s per loop\n" ] } ], "source": [ "%%timeit -r50\n", "convert_bng(lon_ls, lat_ls)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "## Pyproj is now only 1.15x (~15%) faster than Multithreaded Rust, which is 9x faster than pure Python" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Benchmark using cProfile" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Running Rust and Pyproj benchmarks, 1mm conversions x 50 runs\n", "\n", "Rust Benchmark\n", "\n", "Mon May 16 17:41:30 2016 benches/output_stats_rust\n", "\n", " 1370 function calls in 77.970 seconds\n", "\n", " Ordered by: cumulative time\n", " List reduced from 22 to 5 due to restriction <5>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", " 1 44.115 44.115 77.970 77.970 benches/cprofile_rust.py:1()\n", " 50 24.738 0.495 24.739 0.495 benches/cprofile_rust.py:58(_void_array_to_list)\n", " 200 0.002 0.000 9.038 0.045 benches/cprofile_rust.py:18(from_param)\n", " 100 9.032 0.090 9.036 0.090 benches/cprofile_rust.py:23(__init__)\n", " 2 0.044 0.022 0.044 0.022 {method 'uniform' of 'mtrand.RandomState' objects}\n", "\n", "\n", "Pyproj Benchmark\n", "\n", "Mon May 16 17:42:04 2016 benches/output_stats_pyproj\n", "\n", " 822 function calls in 34.655 seconds\n", "\n", " Ordered by: cumulative time\n", " List reduced from 20 to 5 due to restriction <5>\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", " 1 1.223 1.223 34.655 34.655 benches/cprofile_pyproj.py:1()\n", " 50 0.002 0.000 33.382 0.668 /Users/sth/dev/lonlat_bng/venv/lib/python2.7/site-packages/pyproj/__init__.py:418(transform)\n", " 50 23.680 0.474 23.681 0.474 {_proj._transform}\n", " 100 8.043 0.080 8.044 0.080 /Users/sth/dev/lonlat_bng/venv/lib/python2.7/site-packages/pyproj/__init__.py:521(_copytobuffer)\n", " 100 0.001 0.000 1.655 0.017 /Users/sth/dev/lonlat_bng/venv/lib/python2.7/site-packages/pyproj/__init__.py:577(_convertback)\n", "\n", "\n" ] } ], "source": [ "%run remote_bench.py" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# make graph text look good - only run this if you know what you're doing\n", "from matplotlib import rc\n", "rc('font', **{'family':'sans-serif',\n", " 'sans-serif':['Helvetica'],\n", " 'monospace': ['Inconsolata'],\n", " 'serif': ['Helvetica']})\n", "rc('text', **{'usetex': True})\n", "rc('text', **{'latex.preamble': '\\usepackage{sfmath}'})" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
timeerrorivariablenum_pointscoresmethod
0363824982266764911000002crossbeam
1160957371025697221000002crossbeam
2162198171007994041000002crossbeam
3164548411023687181000002crossbeam
41675599110263327161000002crossbeam
51785689510031590321000002crossbeam
6162075841084701211000002rayon
716657233994921121000002rayon
8163863621114455641000002rayon
916000969100382071001000002rayon
1016379867104611112001000002rayon
1116720910107487404001000002rayon
12227245751384545411000008crossbeam
1311520465677566121000008crossbeam
143229282187554441000008crossbeam
152965449177299481000008crossbeam
1629732101911177161000008crossbeam
1733885711828989321000008crossbeam
182888731196510111000008rayon
192877832183609521000008rayon
\n", "
" ], "text/plain": [ " time error ivariable num_points cores method\n", "0 36382498 22667649 1 100000 2 crossbeam\n", "1 16095737 10256972 2 100000 2 crossbeam\n", "2 16219817 10079940 4 100000 2 crossbeam\n", "3 16454841 10236871 8 100000 2 crossbeam\n", "4 16755991 10263327 16 100000 2 crossbeam\n", "5 17856895 10031590 32 100000 2 crossbeam\n", "6 16207584 10847012 1 100000 2 rayon\n", "7 16657233 9949211 2 100000 2 rayon\n", "8 16386362 11144556 4 100000 2 rayon\n", "9 16000969 10038207 100 100000 2 rayon\n", "10 16379867 10461111 200 100000 2 rayon\n", "11 16720910 10748740 400 100000 2 rayon\n", "12 22724575 13845454 1 100000 8 crossbeam\n", "13 11520465 6775661 2 100000 8 crossbeam\n", "14 3229282 1875544 4 100000 8 crossbeam\n", "15 2965449 1772994 8 100000 8 crossbeam\n", "16 2973210 1911177 16 100000 8 crossbeam\n", "17 3388571 1828989 32 100000 8 crossbeam\n", "18 2888731 1965101 1 100000 8 rayon\n", "19 2877832 1836095 2 100000 8 rayon" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = pd.read_csv(\"benches/benchmarks.csv\",\n", " dtype={\n", " 'crossbeam': np.float64,\n", " 'crossbeam_error': np.float64,\n", " 'threads': np.float64,\n", " 'rayon': np.float64,\n", " 'rayon_error': np.float64,\n", " 'weight': np.float64,\n", " 'num_points': np.float64,\n", " 'cores': np.float64,\n", " } \n", ")\n", "\n", "df.head(20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Comparing and Optimising Crossbeam and Rayon" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/png": "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\nd/DgQb3zzjvq0aOHLr/8cvXt2zfbISHH0IKDjDt58qTm/z/z9YsFv9A3Nn5D971zn+a+PVfHnz2u\nv5n9N9q6NdmDr9tWVVWlcePGafr06V0cccdiOXz4cKv506ZN0/jx41VVVRWd9/rrr2vcuHEaN26c\nxo8frzfeeEOS9MYbb2jevHnRcnv37tW0adNS2n5knZdffrmmT5+uhoakD/9O2bJly7RmzRotXLhQ\nl112mS6//HJddtlluvvuuzu9biBVx48f1/13369Lv3aplsxdosdve1xlf1qmRx9+VM3NzR1aJ3XH\nKWGqO0hwkHEr/7+VOvT2Ic3bOk/lh8pVfKJY5xw9R9/a+y3dtuM2PfL3j6R9UlVXV2vUqFHasGGD\nRowYoWeffTag6Ns3btw4VVdXt5pfXV2tiy++WOvXr5ck7dy5Uw0NDXrooYf04osvasOGDVq5cqVm\nzpwZ/T/xD8ls46GZUbHrfOuttzRjxgzdeeedndwr6eabb1ZlZaXmzp2rt99+W2+99Zauvvpq/eAH\nP+j0uoFU3Xfnfdq5Yqde+PwF/ezwz7To8CI9d/Q5/fvSf9dPH/lp2uuj7jglbHUHt6iQUSdOnNDq\nX67W7f9zu05zrT9+wz4bpuGHhmvDv2/Q1Gmpj+lXUlKiq6++WpJ0/vnnt1jW0NCgW2+9VfX19bry\nyit1xx136NZbb5WZqX///lq6dKmWLVumd999V/v27dPy5cs1bdo01dfX62tf+5p+8pOfaMeOHXrg\ngQdkZpoxY4YqKyuTxrJhw4akV4KxiVv//v21atUqzZgxI9q83r9/f7333nvRMrEP4nTOyTmnNWvW\n6KWXXlJ9fb3efffdFuUltVpnZWWlRowYET0O6ex3rGXLlmnQoEHRfd+7d68kaciQIUmPBdCV/vjH\nP+qll17Sqs9Xqa9O3ZI6W2drQeMC/cWSv9Btt9+W1u0q6o5TwlZ30IKDjPrDH/6gXsd66UtHv5S0\nzMg/jtT2/9ye1nqHDx+ufv36afXq1Xr22Wc1ZcqU6LLHH39c3/ve96JXP48//rgmT56s5cuX6/zz\nz49eMZmZli9froULF2ry5Mlav369+vXrp+rqaq1atUq33HKLli9froMHD7YbT6InhE+cOFGvv/66\nvvKVr2jfvn0aMmSI3n333WilWl1dHW1q3rlzpyTp5Zdf1vjx4zV+/HjNnDlTZqbKykotXbpU559/\nvn7609ZXrHv27GlVUQ8ZMkSPP/64pkyZkvJ+r1mzps19/PnPf64f/ehH7R4LoKu89tpr+kaPb7RI\nbiLO1tn6cs8v6+23305rndQdp4St7iDBQUY551RgbX/sClSQ8CRvzwMPPKCXX35ZL7zwgvr16xed\nv2PHDl155ZWSpPvvv1/79u3TVVddJUkaOXKk9u3bJ0m6+OKLJXkn+erVq1VVVaWdO3fq0KFDuuOO\nO/Taa69p+vTpGjhwYNqxReK7/fbb9bvf/U5XXnmlli1bptLSUu3Zs0eSV4lt2LBBV1xxRbQinDJl\nitavX6/169dr6dKl0XWtXr1axcXFmjBhQqvtlJSURNcZcdddd2nfvn3R45DKfrdVGTc0NGjfvn0t\njjMQtObmZvVQj6TLe6qnTp48mfZ6qTs8Yas7SHCQUV/60pfU2KNR+0/fn7TMjrN26E8v+dO01hu5\noli6dGmr5umLL75Yr7/+uiRp3rx5Ovvss6PT27Zti16xRCqf0tJSzZgxQ0uXLtXcuXN11VVXadWq\nVbr99tu1fPlyPf7442nFFnH48OHoNgYOHCgz00033aSFCxdGm5/r6+vb7QOwd+9eLVu2TPfff3/C\n5VOmTNGzzz4bXefq1atVX1+v888/P+39TuaNN96INusDmXLFFVdo08lNOqqjrZbVqU47ju1Ie0Bm\n6o5TwlZ3kOAgo3r27KlxE8bppQteUrNa/+LhD73/oK2DturPKv8srfW+/vrrWrNmTbSHfuyJfscd\nd+iZZ57R+PHjNWDAAN19991avXq1pk+frn379rW6J37TTTfp6aef1rRp01RdXa0hQ4bo/PPP1403\n3qjp06dHT84dO3a0+EVDrNgOfZFyP/rRj/T4449r/Pjx2r59u2666Sb1799fjz76qKZOnarx48fr\nrrvuarfp9sEHH1R9fb2qqqoSbr9///66//77o+t89tln9dOf/lR33HFHSvs9ffr06H4ns2rVKk2c\nOLHNOIGuVlJSoquvuVp/f/rft0hyjuiIflz4Y/35//nztFtJqDtOCVvdEdho4l2B0cS7p6VLl7b5\nNNLPP/9cP573YzX/d7P+7IM/07Ajw9TUo0lvnfGW1pes12133tZtWgeeffZZ3XTTTdkOI+c89NBD\nLSpQRhNHKtqrOySpsbFRfzvnb/Ufv/oPje4xWid0Qr85+Rt9+9vf1kOPPaSePXtmKNrOoe5ILL7u\n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\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.clf()\n", "fig = plt.figure(figsize=(8, 6))\n", "\n", "# Crossbeam\n", "# filter by cores and method\n", "df_cb = df[(df.cores == 2) & (df.method == \"crossbeam\")]\n", "ax1 = fig.add_subplot(121, axisbg='#d3d3d3')\n", "threads = plt.scatter(df_cb['ivariable'], df_cb['time'],\n", " color='#CC00CC',\n", " edgecolor='#333333',\n", " marker='o',\n", " lw=1,\n", " s=80,\n", " alpha=1.0,\n", " zorder=2,\n", ")\n", "ax1.set_ylim(0, 40000000)\n", "plt.errorbar(df_cb['ivariable'], df_cb['time'], yerr=df_cb['error'],\n", " color='#ff96ca',\n", " linestyle=\"None\",\n", " lw=2.,\n", " alpha=0.75,\n", " zorder=1 \n", ")\n", "# filter by cores and method (8 cores)\n", "df_cb_ = df[(df.cores == 8) & (df.method == \"crossbeam\")]\n", "threads_ = plt.scatter(df_cb_['ivariable'], df_cb_['time'],\n", " color='#008080',\n", " edgecolor='#333333',\n", " marker='o',\n", " lw=1,\n", " s=80,\n", " alpha=1.0,\n", " zorder=2,\n", ")\n", "plt.errorbar(df_cb_['ivariable'].values, df_cb_['time'].values, yerr=df_cb_['error'].values,\n", " linestyle=\"None\",\n", " color='#cc8400',\n", " lw=2.,\n", " alpha=0.75,\n", " zorder=1 \n", ")\n", "\n", "plt.ylabel(\"Time (ns)\")\n", "plt.xlabel(\"Num. threads\")\n", "# highlight fastest runs\n", "rd = plt.axhline(df_cb.time.min(), color='red')\n", "rd_ = plt.axhline(df_cb_.time.min(), color='red')\n", "# Legend\n", "leg = plt.legend(\n", " (threads, threads_, rd),\n", " ('2 cores, 1.8GHz Core i7', '8 cores, 3.4GHz Core i7', 'Fastest run'),\n", " loc='upper right',\n", " scatterpoints=1,\n", " fontsize=9)\n", "leg.get_frame().set_alpha(0.5)\n", "ax1.spines['top'].set_visible(False)\n", "ax1.spines['right'].set_visible(False)\n", "\n", "# x ticks\n", "plt.tick_params(\n", " axis='x',\n", " which='both',\n", " top='off',\n", " bottom='on',\n", " labelbottom='on')\n", "\n", "# y ticks\n", "plt.tick_params(\n", " axis='y',\n", " which='both',\n", " left='on',\n", " right='off',\n", " labelbottom='off')\n", "\n", "\n", "plt.title(\"Crossbeam, 100k Points\", fontsize=12)\n", "plt.tight_layout()\n", "\n", "# Rayon\n", "# filter by cores and method\n", "df_ry = df[(df.cores == 2) & (df.method == \"rayon\")]\n", "ax2 = fig.add_subplot(122, axisbg='#d3d3d3')\n", "rayon = plt.scatter(df_ry['ivariable'], df_ry['time'],\n", " color='#CC00CC',\n", " edgecolor='#000000',\n", " marker='o',\n", " lw=1,\n", " s=60,\n", " alpha=1.0,\n", " zorder=2,\n", ")\n", "ax2.set_ylim(0, 40000000)\n", "# pandas bug, so use .values\n", "plt.errorbar(df_ry['ivariable'].values, df_ry['time'].values, yerr=df_ry['error'].values,\n", " linestyle=\"None\",\n", " color='#ff96ca',\n", " lw=2.,\n", " alpha=0.75,\n", " zorder=1 \n", ")\n", "\n", "# filter by cores and method (8 cores)\n", "df_ry_ = df[(df.cores == 8) & (df.method == \"rayon\")]\n", "rayon_ = plt.scatter(df_ry_['ivariable'], df_ry_['time'],\n", " color='#008080',\n", " edgecolor='#333333',\n", " marker='o',\n", " lw=1,\n", " s=80,\n", " alpha=1.0,\n", " zorder=2,\n", ")\n", "plt.errorbar(df_ry_['ivariable'].values, df_ry_['time'].values, yerr=df_ry_['error'].values,\n", " linestyle=\"None\",\n", " color='#cc8400',\n", " lw=2.,\n", " alpha=0.75,\n", " zorder=1 \n", ")\n", "\n", "plt.xlabel(\"Weight\")\n", "ax2.set_yticklabels([])\n", "# highlight fastest runs\n", "ry = plt.axhline(df_ry.time.min(), color='red')\n", "ry_ = plt.axhline(df_ry_.time.min(), color='red')\n", "ax2.spines['top'].set_visible(False)\n", "ax2.spines['right'].set_visible(True)\n", "ax2.spines['left'].set_visible(False)\n", "plt.title(\"Rayon, 100k Points\", fontsize=12)\n", "\n", "leg = plt.legend(\n", " (rayon, rayon_, ry),\n", " ('2 cores, 1.8GHz Core i7', '8 cores, 3.4GHz Core i7', 'Fastest run'),\n", " loc='upper right',\n", " scatterpoints=1,\n", " fontsize=9)\n", "leg.get_frame().set_alpha(0.5)\n", "\n", "# x ticks\n", "# x ticks\n", "plt.tick_params(\n", " axis='x',\n", " which='both',\n", " top='off',\n", " bottom='on',\n", " labelbottom='on')\n", "\n", "# y ticks\n", "plt.tick_params(\n", " axis='y',\n", " which='both',\n", " left='off',\n", " right='on',\n", " labelbottom='off')\n", "\n", "# output\n", "plt.tight_layout()\n", "plt.savefig(\n", " 'crossbeam_v_rayon.png',\n", " format=\"png\", bbox_inches='tight',\n", " alpha=True, dpi=100)\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 2", "language": "python", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.10" } }, "nbformat": 4, "nbformat_minor": 0 }