{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Using `pybind11`\n", "\n", "The package `pybind11` is provides an elegant way to wrap C++ code for Python, including automatic conversions for `numpy` arrays and the C++ `Eigen` linear algebra library. Used with the `cppimport` package, this provides a very nice work flow for integrating C++ and Python:\n", "\n", "- Edit C++ code\n", "- Run Python code" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```bash\n", "! pip install pybind11\n", "! pip install cppimport\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Clone the Eigen library if necessary - no installation is required as Eigen is a header only library." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```bash\n", "! git clone https://github.com/RLovelett/eigen.git\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Resources\n", "\n", "- [`pybind11`](http://pybind11.readthedocs.io/en/latest/)\n", "- [`cppimport`](https://github.com/tbenthompson/cppimport)\n", "- [`Eigen`](http://eigen.tuxfamily.org)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A first example of using `pybind11`\n", "\n", "Create a new subdirectory - e.g. `example1` and create the following 5 files in it:\n", "\n", "- `funcs.hpp`\n", "- `funcs.cpp`\n", "- `wrap.cpp`\n", "- `setup.py`\n", "- `test_funcs.py`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First write the C++ header and implementation files" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks/example1\n" ] } ], "source": [ "%mkdir example1\n", "%cd example1" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing funcs.hpp\n" ] } ], "source": [ "%%file funcs.hpp\n", "\n", "int add(int i, int j);" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing funcs.cpp\n" ] } ], "source": [ "%%file funcs.cpp\n", "\n", "int add(int i, int j) {\n", " return i + j;\n", "};" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next write the C++ wrapper code using `pybind11` in `wrap.cpp`. The arguments `\"i\"_a=1, \"j\"_a=2` in the exported function definition tells `pybind11` to generate variables named `i` with default value 1 and `j` with default value 2 for the `add` function." ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting wrap1.cpp\n" ] } ], "source": [ "%%file wrap1.cpp\n", "#include \n", "#include \"funcs.hpp\"\n", "\n", "namespace py = pybind11;\n", "\n", "using namespace pybind11::literals;\n", "\n", "PYBIND11_MODULE(wrap1, m) {\n", " m.doc() = \"pybind11 example plugin\"; \n", " m.def(\"add\", &add, \"A function which adds two numbers\",\n", " \"i\"_a=1, \"j\"_a=2);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, write the `setup.py` file to compile the extension module. This is mostly boilerplate." ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting setup.py\n" ] } ], "source": [ "%%file setup.py\n", "import os, sys\n", "\n", "from distutils.core import setup, Extension\n", "from distutils import sysconfig\n", "\n", "cpp_args = ['-std=c++11'] \n", "\n", "ext_modules = [\n", " Extension(\n", " 'wrap1',\n", " ['funcs.cpp', 'wrap1.cpp'],\n", " include_dirs=['pybind11/include'],\n", " language='c++',\n", " extra_compile_args = cpp_args,\n", " ),\n", "]\n", "\n", "setup(\n", " name='wrap1',\n", " version='0.0.1',\n", " author='Cliburn Chan',\n", " author_email='cliburn.chan@duke.edu',\n", " description='Example',\n", " ext_modules=ext_modules,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now build the extension module in the subdirectory with these files" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "running build_ext\n", "building 'wrap1' extension\n", "gcc -pthread -B /opt/conda/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -Ipybind11/include -I/opt/conda/include/python3.6m -c funcs.cpp -o build/temp.linux-x86_64-3.6/funcs.o -std=c++11\n", "gcc -pthread -B /opt/conda/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -Ipybind11/include -I/opt/conda/include/python3.6m -c wrap1.cpp -o build/temp.linux-x86_64-3.6/wrap1.o -std=c++11\n", "g++ -pthread -shared -B /opt/conda/compiler_compat -L/opt/conda/lib -Wl,-rpath=/opt/conda/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.6/funcs.o build/temp.linux-x86_64-3.6/wrap1.o -L/opt/conda/lib -lpython3.6m -o /home/jovyan/work/sta-663-2018/notebooks/example6/wrap1.cpython-36m-x86_64-linux-gnu.so\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid for C/ObjC but not for C++\n", "cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid for C/ObjC but not for C++\n" ] } ], "source": [ "%%bash\n", "\n", "python setup.py build_ext -i" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And if you are successful, you should now see a new ```funcs.so``` extension module. We can write a `test_funcs.py` file test the extension module:" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing test_funcs.py\n" ] } ], "source": [ "%%file test_funcs.py\n", "\n", "import wrap1\n", "\n", "def test_add():\n", " print(wrap1.add(3, 4))\n", " assert(wrap1.add(3, 4) == 7)\n", "\n", "if __name__ == '__main__':\n", " test_add()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And finally, running the test should not generate any error messages:" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "7\n" ] } ], "source": [ "%%bash\n", "\n", "python test_funcs.py" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks\n" ] } ], "source": [ "%cd .." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using `cppimport`\n", "\n", "In the development stage, it can be distracting to have to repeatedly rebuild the extension module by running\n", "\n", "```bash\n", "python setup.py clean\n", "python setup.py build_ext -i\n", "```\n", "\n", "every single time you modify the C++ code. The `cppimport` package does this for you. \n", "\n", "Create a new sub-directory `exaample2` and copy the files `func.hpp`, `funcs.cpp` and `wrap.cpp` from `example1` over.\n", "For the previous example, we just need to add some annotation (between `<% and %>` delimiters) to the top of the `wrap.cpp` file" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mkdir: cannot create directory ‘example2’: File exists\n", "/home/jovyan/work/sta-663-2018/notebooks/example2\n" ] } ], "source": [ "%mkdir example2\n", "%cp example1/funcs.* example2/\n", "%cd example2" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting wrap2.cpp\n" ] } ], "source": [ "%%file wrap2.cpp\n", "<%\n", "cfg['compiler_args'] = ['-std=c++11'] \n", "cfg['sources'] = ['funcs.cpp']\n", "setup_pybind11(cfg)\n", "%>\n", "\n", "#include \"funcs.hpp\"\n", "#include \n", "\n", "namespace py = pybind11;\n", "\n", "PYBIND11_MODULE(wrap2, m) {\n", " m.doc() = \"pybind11 example plugin\"; \n", " m.def(\"add\", &add, \"A function which adds two numbers\");\n", "}" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting test_funcs.py\n" ] } ], "source": [ "%%file test_funcs.py\n", "\n", "import cppimport\n", "funcs = cppimport.imp(\"wrap2\")\n", "\n", "def test_add():\n", " assert(funcs.add(3, 4) == 7)\n", "\n", "if __name__ == '__main__':\n", " print(funcs.add(3,4))\n", " test_add()" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "7\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid for C/ObjC but not for C++\n", "cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid for C/ObjC but not for C++\n" ] } ], "source": [ "%%bash\n", "\n", "python test_funcs.py " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Use of `cppimport`\n", "\n", "Note that `cppimport.imp` is only called once. Once it is called, the shared library is created and can be sued" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "wrap2.cpython-36m-x86_64-linux-gnu.so\r\n" ] } ], "source": [ "! ls *so" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That is, you can import wrap2 and call from notebook" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "7" ] }, "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import wrap2\n", "\n", "wrap2.add(3, 4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "without any need to manually build the extension module. Any updates will be detected by `cppimport` and it will automatically trigger a re-build." ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks\n" ] } ], "source": [ "%cd .." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Vectorizing functions for use with `numpy` arrays\n", "\n", "Example showing how to vectorize a `square` function. Note that from here on, we don't bother to use separate header and implementation files for these code snippets, and just write them together with the wrapping code in a `code.cpp` file. This means that with `cppimport`, there are only two files that we actually code for, a C++ `code.cpp` file and a python test file." ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mkdir: cannot create directory ‘example3’: File exists\n", "/home/jovyan/work/sta-663-2018/notebooks/example3\n" ] } ], "source": [ "%mkdir example3\n", "%cd example3" ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting wrap3.cpp\n" ] } ], "source": [ "%%file wrap3.cpp\n", "<%\n", "cfg['compiler_args'] = ['-std=c++11'] \n", "setup_pybind11(cfg)\n", "%>\n", "\n", "#include \n", "#include \n", "\n", "namespace py = pybind11;\n", "double square(double x) {\n", " return x * x;\n", "}\n", "\n", "PYBIND11_MODULE(wrap3, m) {\n", " m.doc() = \"pybind11 example plugin\";\n", " m.def(\"square\", py::vectorize(square), \"A vectroized square function.\");\n", "}" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 1., 4., 9.])" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import cppimport\n", "\n", "wrap3 = cppimport.imp(\"wrap3\")\n", "wrap3.square([1,2,3])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once the shared libary is built, you can use it as a regular Python module." ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "wrap3.cpp wrap3.cpython-36m-x86_64-linux-gnu.so\r\n" ] } ], "source": [ "! ls" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 4., 16., 36.])" ] }, "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import wrap3\n", "\n", "wrap3.square([2,4,6])" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks\n" ] } ], "source": [ "%cd .." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using `numpy` arrays as function arguments and return values\n", "\n", "Example showing how to pass `numpy` arrays in and out of functions. These `numpy` array arguments can either be generic `py:array` or typed `py:array_t`. The properties of the `numpy` array can be obtained by calling its `request` method. This returns a `struct` of the following form:\n", "\n", "```c++\n", "struct buffer_info {\n", " void *ptr;\n", " size_t itemsize;\n", " std::string format;\n", " int ndim;\n", " std::vector shape;\n", " std::vector strides;\n", "};\n", "```\n", "\n", "Here is C++ code for two functions - the function `twice` shows how to change a passed in `numpy` array in-place using pointers; the function `sum` shows how to sum the elements of a `numpy` array. By taking advantage of the information in `buffer_info`, the code will work for arbitrary `n-d` arrays." ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mkdir: cannot create directory ‘example4’: File exists\n", "/home/jovyan/work/sta-663-2018/notebooks/example4\n" ] } ], "source": [ "%mkdir example4\n", "%cd example4" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting wrap4.cpp\n" ] } ], "source": [ "%%file wrap4.cpp\n", "<%\n", "cfg['compiler_args'] = ['-std=c++11']\n", "setup_pybind11(cfg)\n", "%>\n", "\n", "#include \n", "#include \n", "\n", "namespace py = pybind11;\n", "\n", "// Passing in an array of doubles\n", "void twice(py::array_t xs) {\n", " py::buffer_info info = xs.request();\n", " auto ptr = static_cast(info.ptr);\n", "\n", " int n = 1;\n", " for (auto r: info.shape) {\n", " n *= r;\n", " }\n", "\n", " for (int i = 0; i (info.ptr);\n", "\n", " int n = 1;\n", " for (auto r: info.shape) {\n", " n *= r;\n", " }\n", "\n", " double s = 0.0;\n", " for (int i = 0; i \n", "\n", "#include \n", "#include \n", "\n", "namespace py = pybind11;\n", "\n", "py::array_t add_arrays(py::array_t input1, py::array_t input2) {\n", " auto buf1 = input1.request(), buf2 = input2.request();\n", "\n", " if (buf1.ndim != 1 || buf2.ndim != 1)\n", " throw std::runtime_error(\"Number of dimensions must be one\");\n", "\n", " if (buf1.shape[0] != buf2.shape[0])\n", " throw std::runtime_error(\"Input shapes must match\");\n", "\n", " auto result = py::array(py::buffer_info(\n", " nullptr, /* Pointer to data (nullptr -> ask NumPy to allocate!) */\n", " sizeof(double), /* Size of one item */\n", " py::format_descriptor::value, /* Buffer format */\n", " buf1.ndim, /* How many dimensions? */\n", " { buf1.shape[0] }, /* Number of elements for each dimension */\n", " { sizeof(double) } /* Strides for each dimension */\n", " ));\n", "\n", " auto buf3 = result.request();\n", "\n", " double *ptr1 = (double *) buf1.ptr,\n", " *ptr2 = (double *) buf2.ptr,\n", " *ptr3 = (double *) buf3.ptr;\n", "\n", " for (size_t idx = 0; idx < buf1.shape[0]; idx++)\n", " ptr3[idx] = ptr1[idx] + ptr2[idx];\n", "\n", " return result;\n", "}\n", "\n", "PYBIND11_MODULE(wrap5, m) {\n", " m.def(\"add_arrays\", &add_arrays, \"Add two NumPy arrays\");\n", "}" ] }, { "cell_type": "code", "execution_count": 78, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 0 1 2 3 4 5 6 7 8 9 10 11]\n", "[ 0. 2. 4. 6. 8. 10. 12. 14. 16. 18. 20. 22.]\n" ] } ], "source": [ "import cppimport\n", "import numpy as np\n", "\n", "code = cppimport.imp(\"wrap5\")\n", "\n", "xs = np.arange(12)\n", "print(xs)\n", "\n", "print(code.add_arrays(xs, xs))" ] }, { "cell_type": "code", "execution_count": 79, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks\n" ] } ], "source": [ "%cd .." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using the C++ `eigen` library to calculate matrix inverse and determinant\n", "\n", "Example showing how `Eigen` vectors and matrices can be passed in and out of C++ functions. Note that `Eigen` arrays are automatically converted to/from `numpy` arrays simply by including the `pybind/eigen.h` header. Because of this, it is probably simplest in most cases to work with `Eigen` vectors and matrices rather than `py::buffer` or `py::array` where `py::vectorize` is insufficient." ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mkdir: cannot create directory ‘example6’: File exists\n", "/home/jovyan/work/sta-663-2018/notebooks/example6\n" ] } ], "source": [ "%mkdir example6\n", "%cd example6" ] }, { "cell_type": "code", "execution_count": 85, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting wrap6.cpp\n" ] } ], "source": [ "%%file wrap6.cpp\n", "<%\n", "cfg['compiler_args'] = ['-std=c++11']\n", "cfg['include_dirs'] = ['../eigen3']\n", "setup_pybind11(cfg)\n", "%>\n", "\n", "#include \n", "#include \n", "\n", "#include \n", "\n", "namespace py = pybind11;\n", "\n", "// convenient matrix indexing comes for free\n", "double get(Eigen::MatrixXd xs, int i, int j) {\n", " return xs(i, j);\n", "}\n", "\n", "// takes numpy array as input and returns double\n", "double det(Eigen::MatrixXd xs) {\n", " return xs.determinant();\n", "}\n", "\n", "// takes numpy array as input and returns another numpy array\n", "Eigen::MatrixXd inv(Eigen::MatrixXd xs) {\n", " return xs.inverse();\n", "}\n", "\n", "PYBIND11_MODULE(wrap6, m) {\n", " m.doc() = \"auto-compiled c++ extension\";\n", " m.def(\"inv\", &inv);\n", " m.def(\"det\", &det);\n", "}" ] }, { "cell_type": "code", "execution_count": 86, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[ 1 2 1]\n", " [ 2 1 0]\n", " [-1 1 2]]\n", "-3.0\n", "[[-0.66666667 1. 0.33333333]\n", " [ 1.33333333 -1. -0.66666667]\n", " [-1. 1. 1. ]]\n" ] } ], "source": [ "import cppimport\n", "import numpy as np\n", "\n", "code = cppimport.imp(\"wrap6\")\n", "\n", "A = np.array([[1,2,1],\n", " [2,1,0],\n", " [-1,1,2]])\n", "\n", "print(A)\n", "print(code.det(A))\n", "print(code.inv(A))" ] }, { "cell_type": "code", "execution_count": 87, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks\n" ] } ], "source": [ "%cd .." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using `pybind11` with `openmp`" ] }, { "cell_type": "code", "execution_count": 88, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks/example7\n" ] } ], "source": [ "%mkdir example7\n", "%cd example7" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is an example of using OpenMP to integrate the value of $\\pi$ written using `pybind11`." ] }, { "cell_type": "code", "execution_count": 89, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing wrap7.cpp\n" ] } ], "source": [ "%%file wrap7.cpp\n", "/*\n", "<%\n", "cfg['compiler_args'] = ['-std=c++11', '-fopenmp']\n", "cfg['linker_args'] = ['-lgomp']\n", "setup_pybind11(cfg)\n", "%>\n", "*/\n", "#include \n", "#include \n", "#include \n", "#include \n", "\n", "namespace py = pybind11;\n", "\n", "// Passing in an array of doubles\n", "void twice(py::array_t xs) {\n", " py::gil_scoped_acquire acquire;\n", " \n", " py::buffer_info info = xs.request();\n", " auto ptr = static_cast(info.ptr);\n", "\n", " int n = 1;\n", " for (auto r: info.shape) {\n", " n *= r;\n", " }\n", "\n", " #pragma omp parallel for\n", " for (int i = 0; i xs) {\n", " /* Release GIL before calling into C++ code */ \n", " py::gil_scoped_release release;\n", " return twice(xs);\n", " });\n", "}" ] }, { "cell_type": "code", "execution_count": 90, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 0., 2., 4., 6., 8., 10., 12., 14., 16., 9.])" ] }, "execution_count": 90, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import cppimport\n", "import numpy as np\n", "\n", "code = cppimport.imp(\"wrap7\")\n", "xs = np.arange(10).astype('double')\n", "code.twice(xs)\n", "xs" ] }, { "cell_type": "code", "execution_count": 91, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/jovyan/work/sta-663-2018/notebooks\n" ] } ], "source": [ "%cd .." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.5" }, "latex_envs": { "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 0 } }, "nbformat": 4, "nbformat_minor": 1 }