{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import open3d.core as o3c\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tensor\n",
    "\n",
    "Tensor is a \"view\" of a data Blob with shape, stride, and a data pointer. It is a multidimensional and homogeneous matrix containing elements of single data type. It is used in Open3D to perform numerical operations. It supports GPU operations as well.\n",
    "\n",
    "## Tensor creation\n",
    "\n",
    "Tensor can be created from list, numpy array, another tensor. A tensor of specific data type and device can be constructed by passing a ```o3c.Dtype``` and/or ```o3c.Device``` to a constructor. If not passed, the default data type is inferred from the data, and the default device is CPU.\n",
    "Note that while creating tensor from a list or numpy array, the underlying memory is not shared and a copy is created."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Created from list:\n",
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d0575a48f0]\n",
      "\n",
      "Created from numpy array:\n",
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d056caa8d0]\n",
      "\n",
      "Default dtype and device:\n",
      "[0.0 1.0 2.0]\n",
      "Tensor[shape={3}, stride={1}, Float64, CPU:0, 0x55d05759c330]\n",
      "\n",
      "Specified data type:\n",
      "[0.0 1.0 2.0]\n",
      "Tensor[shape={3}, stride={1}, Float64, CPU:0, 0x55d057568ca0]\n",
      "\n",
      "Specified device:\n",
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int64, CUDA:0, 0x7f40ff000000]\n"
     ]
    }
   ],
   "source": [
    "# Tensor from list.\n",
    "a = o3c.Tensor([0, 1, 2])\n",
    "print(\"Created from list:\\n{}\".format(a))\n",
    "\n",
    "# Tensor from Numpy.\n",
    "a = o3c.Tensor(np.array([0, 1, 2]))\n",
    "print(\"\\nCreated from numpy array:\\n{}\".format(a))\n",
    "\n",
    "# Dtype and inferred from list.\n",
    "a_float = o3c.Tensor([0.0, 1.0, 2.0])\n",
    "print(\"\\nDefault dtype and device:\\n{}\".format(a_float))\n",
    "\n",
    "# Specify dtype.\n",
    "a = o3c.Tensor(np.array([0, 1, 2]), dtype=o3c.Dtype.Float64)\n",
    "print(\"\\nSpecified data type:\\n{}\".format(a))\n",
    "\n",
    "# Specify device.\n",
    "a = o3c.Tensor(np.array([0, 1, 2]), device=o3c.Device(\"CUDA:0\"))\n",
    "print(\"\\nSpecified device:\\n{}\".format(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "   Tensor can also be created from another tensor by invoking the copy constructor. This is a shallow copy, the data_ptr will be copied but the memory it points to will not be copied."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Source tensor:\n",
      "[11 2 3]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d057b157a0]\n",
      "\n",
      "Target tensor:\n",
      "[11 2 3]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d057b157a0]\n"
     ]
    }
   ],
   "source": [
    "# Shallow copy constructor.\n",
    "vals = np.array([1, 2, 3])\n",
    "src = o3c.Tensor(vals)\n",
    "dst = src\n",
    "src[0] += 10\n",
    "\n",
    "# Changes in one will get reflected in other.\n",
    "print(\"Source tensor:\\n{}\".format(src))\n",
    "print(\"\\nTarget tensor:\\n{}\".format(dst))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Properties of a tensor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a.shape: SizeVector[2, 3, 4]\n",
      "a.strides: SizeVector[12, 4, 1]\n",
      "a.dtype: Float64\n",
      "a.device: CUDA:0\n",
      "a.ndim: 3\n"
     ]
    }
   ],
   "source": [
    "vals = np.array((range(24))).reshape(2, 3, 4)\n",
    "a = o3c.Tensor(vals, dtype=o3c.Dtype.Float64, device=o3c.Device(\"CUDA:0\"))\n",
    "print(f\"a.shape: {a.shape}\")\n",
    "print(f\"a.strides: {a.strides}\")\n",
    "print(f\"a.dtype: {a.dtype}\")\n",
    "print(f\"a.device: {a.device}\")\n",
    "print(f\"a.ndim: {a.ndim}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Copy & device transfer\n",
    "We can transfer tensors across host and multiple devices."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int64, CUDA:0, 0x7f40ff000000]\n",
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d05c254780]\n",
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int64, CUDA:0, 0x7f40ff000000]\n"
     ]
    }
   ],
   "source": [
    "# Host -> Device.\n",
    "a_cpu = o3c.Tensor([0, 1, 2])\n",
    "a_gpu = a_cpu.cuda(0)\n",
    "print(a_gpu)\n",
    "\n",
    "# Device -> Host.\n",
    "a_gpu = o3c.Tensor([0, 1, 2], device=o3c.Device(\"CUDA:0\"))\n",
    "a_cpu = a_gpu.cpu()\n",
    "print(a_cpu)\n",
    "\n",
    "# Device -> another Device.\n",
    "a_gpu_0 = o3c.Tensor([0, 1, 2], device=o3c.Device(\"CUDA:0\"))\n",
    "a_gpu_1 = a_gpu_0.cuda(0)\n",
    "print(a_gpu_1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Types\n",
    "\n",
    "Open3D defines several scalar tensor data types.\n",
    "\n",
    "| Data type                 | dtype               | byte_size  |\n",
    "|---------------------------|---------------------|------------|\n",
    "| Uninitialized Tensor      | o3c.Dtype.Undefined | -          |\n",
    "| 32-bit floating point     | o3c.Dtype.Float32   | 4          |\n",
    "| 64-bit floating point     | o3c.Dtype.Float64   | 8          |\n",
    "| 8-bit integer (signed)    | o3c.Dtype.Int8      | 1          |\n",
    "| 16-bit integer (signed)   | o3c.Dtype.Int16     | 2          |\n",
    "| 32-bit integer (signed)   | o3c.Dtype.Int32     | 4          |\n",
    "| 64-bit integer (signed)   | o3c.Dtype.Int64     | 8          |\n",
    "| 8-bit integer (unsigned)  | o3c.Dtype.UInt8     | 1          |\n",
    "| 16-bit integer (unsigned) | o3c.Dtype.UInt16    | 2          |\n",
    "| 32-bit integer (unsigned) | o3c.Dtype.UInt32    | 4          |\n",
    "| 64-bit integer (unsigned) | o3c.Dtype.UInt64    | 8          |\n",
    "| Boolean                   | o3c.Dtype.Bool      | 1          |\n",
    "\n",
    "### Type casting\n",
    "We can cast tensor's data type. Forced casting might result in data loss."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0.1 1.5 2.7]\n",
      "Tensor[shape={3}, stride={1}, Float64, CPU:0, 0x55d056510450]\n",
      "[0 1 2]\n",
      "Tensor[shape={3}, stride={1}, Int32, CPU:0, 0x55d0565104d0]\n"
     ]
    }
   ],
   "source": [
    "# E.g. float -> int\n",
    "a = o3c.Tensor([0.1, 1.5, 2.7])\n",
    "b = a.to(o3c.Dtype.Int32)\n",
    "print(a)\n",
    "print(b)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1 2 3]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d0565103c0]\n",
      "[1.0 2.0 3.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05c9cddd0]\n"
     ]
    }
   ],
   "source": [
    "# E.g. int -> float\n",
    "a = o3c.Tensor([1, 2, 3])\n",
    "b = a.to(o3c.Dtype.Float32)\n",
    "print(a)\n",
    "print(b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Numpy I/O with direct memory map\n",
    "\n",
    "Tensors created by passing numpy array to the constructor(```o3c.Tensor(np.array(...)```) do not share memory with the numpy array. To have shared memory, you can use ```o3c.Tensor.from_numpy(...)``` and ```o3c.Tensor.numpy(...)```. Changes in either of them will get reflected in other."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "np_a: [1 1 1 1 1]\n",
      "o3_a: [1 1 1 1 1]\n",
      "Tensor[shape={5}, stride={1}, Int32, CPU:0, 0x55d057b15480]\n",
      "\n",
      "np_a: [101   1   1   1   1]\n",
      "o3_a: [1 201 1 1 1]\n",
      "Tensor[shape={5}, stride={1}, Int32, CPU:0, 0x55d057b15480]\n"
     ]
    }
   ],
   "source": [
    "# Using constructor.\n",
    "np_a = np.ones((5,), dtype=np.int32)\n",
    "o3_a = o3c.Tensor(np_a)\n",
    "print(f\"np_a: {np_a}\")\n",
    "print(f\"o3_a: {o3_a}\")\n",
    "print(\"\")\n",
    "\n",
    "# Changes to numpy array will not reflect as memory is not shared.\n",
    "np_a[0] += 100\n",
    "o3_a[1] += 200\n",
    "print(f\"np_a: {np_a}\")\n",
    "print(f\"o3_a: {o3_a}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "np_a: [101 201   1   1   1]\n",
      "o3_a: [101 201 1 1 1]\n",
      "Tensor[shape={5}, stride={1}, Int32, CPU:0, 0x55d057b15a60]\n"
     ]
    }
   ],
   "source": [
    "# From numpy.\n",
    "np_a = np.ones((5,), dtype=np.int32)\n",
    "o3_a = o3c.Tensor.from_numpy(np_a)\n",
    "\n",
    "# Changes to numpy array reflects on open3d Tensor and vice versa.\n",
    "np_a[0] += 100\n",
    "o3_a[1] += 200\n",
    "print(f\"np_a: {np_a}\")\n",
    "print(f\"o3_a: {o3_a}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "np_a: [101 201   1   1   1]\n",
      "o3_a: [101 201 1 1 1]\n",
      "Tensor[shape={5}, stride={1}, Int32, CPU:0, 0x55d056cc3d30]\n",
      "\n",
      "o3_a.cpu().numpy(): [1 1 1 1 1]\n"
     ]
    }
   ],
   "source": [
    "# To numpy.\n",
    "o3_a = o3c.Tensor([1, 1, 1, 1, 1], dtype=o3c.Dtype.Int32)\n",
    "np_a = o3_a.numpy()\n",
    "\n",
    "# Changes to numpy array reflects on open3d Tensor and vice versa.\n",
    "np_a[0] += 100\n",
    "o3_a[1] += 200\n",
    "print(f\"np_a: {np_a}\")\n",
    "print(f\"o3_a: {o3_a}\")\n",
    "\n",
    "# For CUDA Tensor, call cpu() before calling numpy().\n",
    "o3_a = o3c.Tensor([1, 1, 1, 1, 1], device=o3c.Device(\"CUDA:0\"))\n",
    "print(f\"\\no3_a.cpu().numpy(): {o3_a.cpu().numpy()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## PyTorch I/O with DLPack memory map\n",
    "We can convert tensors from/to DLManagedTensor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "th_a: tensor([1., 1., 1., 1., 1.], device='cuda:0')\n",
      "o3_a: [1.0 1.0 1.0 1.0 1.0]\n",
      "Tensor[shape={5}, stride={1}, Float32, CUDA:0, 0x7f409be00000]\n",
      "\n",
      "th_a: tensor([100., 200.,   1.,   1.,   1.], device='cuda:0')\n",
      "o3_a: [100.0 200.0 1.0 1.0 1.0]\n",
      "Tensor[shape={5}, stride={1}, Float32, CUDA:0, 0x7f409be00000]\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.utils.dlpack\n",
    "\n",
    "# From PyTorch\n",
    "th_a = torch.ones((5,)).cuda(0)\n",
    "o3_a = o3c.Tensor.from_dlpack(torch.utils.dlpack.to_dlpack(th_a))\n",
    "print(f\"th_a: {th_a}\")\n",
    "print(f\"o3_a: {o3_a}\")\n",
    "print(\"\")\n",
    "\n",
    "# Changes to PyTorch array reflects on open3d Tensor and vice versa\n",
    "th_a[0] = 100\n",
    "o3_a[1] = 200\n",
    "print(f\"th_a: {th_a}\")\n",
    "print(f\"o3_a: {o3_a}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "th_a: tensor([1, 1, 1, 1, 1], device='cuda:0')\n",
      "o3_a: [1 1 1 1 1]\n",
      "Tensor[shape={5}, stride={1}, Int64, CUDA:0, 0x7f40ff000200]\n",
      "\n",
      "th_a: tensor([100, 200,   1,   1,   1], device='cuda:0')\n",
      "o3_a: [100 200 1 1 1]\n",
      "Tensor[shape={5}, stride={1}, Int64, CUDA:0, 0x7f40ff000200]\n"
     ]
    }
   ],
   "source": [
    "# To PyTorch\n",
    "o3_a = o3c.Tensor([1, 1, 1, 1, 1], device=o3c.Device(\"CUDA:0\"))\n",
    "th_a = torch.utils.dlpack.from_dlpack(o3_a.to_dlpack())\n",
    "o3_a = o3c.Tensor.from_dlpack(torch.utils.dlpack.to_dlpack(th_a))\n",
    "print(f\"th_a: {th_a}\")\n",
    "print(f\"o3_a: {o3_a}\")\n",
    "print(\"\")\n",
    "\n",
    "# Changes to PyTorch array reflects on open3d Tensor and vice versa\n",
    "th_a[0] = 100\n",
    "o3_a[1] = 200\n",
    "print(f\"th_a: {th_a}\")\n",
    "print(f\"o3_a: {o3_a}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Binary element-wise operation:\n",
    "\n",
    "Supported element-wise binary operations are:\n",
    "1. `Add(+)`\n",
    "2. `Sub(-)`\n",
    "3. `Mul(*)`\n",
    "4. `Div(/)`\n",
    "5. `Add_(+=)`\n",
    "6. `Sub_(-=)`\n",
    "7. `Mul_(*=)`\n",
    "8. `Div_(/=)`\n",
    "\n",
    "Note that the operands have to be of same Device, dtype and Broadcast compatible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a + b = [3.0 3.0 3.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d0573a0ed0]\n",
      "a - b = [-1.0 -1.0 -1.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed01410]\n",
      "a * b = [2.0 2.0 2.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed0a180]\n",
      "a / b = [0.5 0.5 0.5]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed013f0]\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor([1, 1, 1], dtype=o3c.Dtype.Float32)\n",
    "b = o3c.Tensor([2, 2, 2], dtype=o3c.Dtype.Float32)\n",
    "print(\"a + b = {}\".format(a + b))\n",
    "print(\"a - b = {}\".format(a - b))\n",
    "print(\"a * b = {}\".format(a * b))\n",
    "print(\"a / b = {}\".format(a / b))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Broadcasting follows the same numpy broadcasting rule as given [here](https://numpy.org/doc/stable/user/basics.broadcasting.html).<br>\n",
    "Automatic type casting is done in a way to avoid data loss."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a + b = \n",
      "[[2.0 2.0 2.0],\n",
      " [2.0 2.0 2.0]]\n",
      "Tensor[shape={2, 3}, stride={3, 1}, Float32, CPU:0, 0x55d05ed0a440]\n",
      "\n",
      "a + 1 = [2.0 2.0 2.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed0baa0]\n",
      "a + True = [2.0 2.0 2.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d0565103e0]\n",
      "a = [0.0 0.0 0.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05c2547a0]\n"
     ]
    }
   ],
   "source": [
    "# Automatic broadcasting.\n",
    "a = o3c.Tensor.ones((2, 3), dtype=o3c.Dtype.Float32)\n",
    "b = o3c.Tensor.ones((3,), dtype=o3c.Dtype.Float32)\n",
    "print(\"a + b = \\n{}\\n\".format(a + b))\n",
    "\n",
    "# Automatic type casting.\n",
    "a = a[0]\n",
    "print(\"a + 1 = {}\".format(a + 1))  # Float + Int -> Float.\n",
    "print(\"a + True = {}\".format(a + True))  # Float + Bool -> Float.\n",
    "\n",
    "# Inplace.\n",
    "a -= True\n",
    "print(\"a = {}\".format(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Unary element-wise operation:\n",
    "Supported unary element-wise operations are:\n",
    "1. `sqrt`, `sqrt_`(inplace))\n",
    "2. `sin`, `sin_`\n",
    "3. `cos`, `cos_`\n",
    "4. `neg`, `neg_`\n",
    "5. `exp`, `exp_`\n",
    "6. `abs`, `abs_`\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a = [4.0 9.0 16.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed01410]\n",
      "\n",
      "a.sqrt = [2.0 3.0 4.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d0beec40a0]\n",
      "\n",
      "a.sin = [-0.756802 0.412118 -0.287903]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d056510330]\n",
      "\n",
      "a.cos = [-0.653644 -0.91113 -0.957659]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed013d0]\n",
      "\n",
      "[2.0 3.0 4.0]\n",
      "Tensor[shape={3}, stride={1}, Float32, CPU:0, 0x55d05ed01410]\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor([4, 9, 16], dtype=o3c.Dtype.Float32)\n",
    "print(\"a = {}\\n\".format(a))\n",
    "print(\"a.sqrt = {}\\n\".format(a.sqrt()))\n",
    "print(\"a.sin = {}\\n\".format(a.sin()))\n",
    "print(\"a.cos = {}\\n\".format(a.cos()))\n",
    "\n",
    "# Inplace operation\n",
    "a.sqrt_()\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reduction:\n",
    "\n",
    "Open3D supports following reduction operations.\n",
    "1. `sum` - returns a tensor with sum of values over a given axis.\n",
    "2. `mean` - returns a tensor with mean of values over a given axis.\n",
    "3. `prod` - returns a tensor with product of values over a given axis.\n",
    "4. `min` - returns a tensor of minimum values along a given axis.\n",
    "5. `max` - returns a tensor of maximum values along a given axis.\n",
    "6. `argmin` - returns a tensor of minimum value indices over a given axis.\n",
    "7. `argmax` - returns a tensor of maximum value indices over a given axis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a.sum = 276\n",
      "Tensor[shape={}, stride={}, Int64, CPU:0, 0x55d056cc3d50]\n",
      "\n",
      "a.min = 0\n",
      "Tensor[shape={}, stride={}, Int64, CPU:0, 0x55d0beec4080]\n",
      "\n",
      "a.ArgMax = 23\n",
      "Tensor[shape={}, stride={}, Int64, CPU:0, 0x55d05ed0a440]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "print(\"a.sum = {}\\n\".format(a.sum()))\n",
    "print(\"a.min = {}\\n\".format(a.min()))\n",
    "print(\"a.ArgMax = {}\\n\".format(a.argmax()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Along dim=0\n",
      "[[12 14 16 18],\n",
      " [20 22 24 26],\n",
      " [28 30 32 34]]\n",
      "Tensor[shape={3, 4}, stride={4, 1}, Int64, CPU:0, 0x55d08bffade0]\n",
      "Along dim=(0, 2)\n",
      "[60 92 124]\n",
      "Tensor[shape={3}, stride={1}, Int64, CPU:0, 0x55d0583dd120]\n",
      "\n",
      "Shape without retention : SizeVector[3]\n",
      "Shape with retention : SizeVector[1, 3, 1]\n"
     ]
    }
   ],
   "source": [
    "# With specified dimension.\n",
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "\n",
    "print(\"Along dim=0\\n{}\".format(a.sum(dim=(0))))\n",
    "print(\"Along dim=(0, 2)\\n{}\\n\".format(a.sum(dim=(0, 2))))\n",
    "\n",
    "# Retention of reduced dimension.\n",
    "print(\"Shape without retention : {}\".format(a.sum(dim=(0, 2)).shape))\n",
    "print(\"Shape with retention : {}\".format(a.sum(dim=(0, 2), keepdim=True).shape))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Slicing, indexing, getitem, and setitem\n",
    "\n",
    "Basic slicing is done by passing an integer, slice object(```start:stop:step```), index array or boolean array. Slicing and indexing produce a view of the tensor. Hence any change in it will also get reflected in the original tensor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a = \n",
      "[[[0 1 2 3],\n",
      "  [4 5 6 7],\n",
      "  [8 9 10 11]],\n",
      " [[12 13 14 15],\n",
      "  [16 17 18 19],\n",
      "  [20 21 22 23]]]\n",
      "Tensor[shape={2, 3, 4}, stride={12, 4, 1}, Int64, CPU:0, 0x55d05ed03150]\n",
      "\n",
      "a[1, 2] = [20 21 22 23]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x55d05ed031f0]\n",
      "\n",
      "a[1:] = \n",
      "[[[12 13 14 15],\n",
      "  [16 17 18 19],\n",
      "  [20 21 22 23]]]\n",
      "Tensor[shape={1, 3, 4}, stride={12, 4, 1}, Int64, CPU:0, 0x55d05ed031b0]\n",
      "\n",
      "a[:, 0:3:2, :] = \n",
      "[[[0 1 2 3],\n",
      "  [8 9 10 11]],\n",
      " [[12 13 14 15],\n",
      "  [20 21 22 23]]]\n",
      "Tensor[shape={2, 2, 4}, stride={12, 8, 1}, Int64, CPU:0, 0x55d05ed03150]\n",
      "\n",
      "a[:-1, 0:3:2, 2] = \n",
      "[[2 10]]\n",
      "Tensor[shape={1, 2}, stride={12, 8}, Int64, CPU:0, 0x55d05ed03160]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "print(\"a = \\n{}\\n\".format(a))\n",
    "\n",
    "# Indexing __getitem__.\n",
    "print(\"a[1, 2] = {}\\n\".format(a[1, 2]))\n",
    "\n",
    "# Slicing __getitem__.\n",
    "print(\"a[1:] = \\n{}\\n\".format(a[1:]))\n",
    "\n",
    "# slice object.\n",
    "print(\"a[:, 0:3:2, :] = \\n{}\\n\".format(a[:, 0:3:2, :]))\n",
    "\n",
    "# Combined __getitem__\n",
    "print(\"a[:-1, 0:3:2, 2] = \\n{}\\n\".format(a[:-1, 0:3:2, 2]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "b = [[102 110]]\n",
      "Tensor[shape={1, 2}, stride={12, 8}, Int64, CPU:0, 0x55d05ed01160]\n",
      "\n",
      "a = \n",
      "[[[0 1 102 3],\n",
      "  [4 5 6 7],\n",
      "  [8 9 110 11]],\n",
      " [[12 13 14 15],\n",
      "  [16 17 18 19],\n",
      "  [20 21 22 23]]]\n",
      "Tensor[shape={2, 3, 4}, stride={12, 4, 1}, Int64, CPU:0, 0x55d05ed01150]\n"
     ]
    }
   ],
   "source": [
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "\n",
    "# Changes get reflected.\n",
    "b = a[:-1, 0:3:2, 2]\n",
    "b[0] += 100\n",
    "print(\"b = {}\\n\".format(b))\n",
    "print(\"a = \\n{}\".format(a))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[[0 1 102 3],\n",
      "  [4 5 106 7],\n",
      "  [8 9 110 11]],\n",
      " [[12 13 114 15],\n",
      "  [16 17 118 19],\n",
      "  [20 21 122 23]]]\n",
      "Tensor[shape={2, 3, 4}, stride={12, 4, 1}, Int64, CPU:0, 0x55d0573a0ed0]\n"
     ]
    }
   ],
   "source": [
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "\n",
    "# Example __setitem__\n",
    "a[:, :, 2] += 100\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Advanced indexing\n",
    "\n",
    "Advanced indexing is triggered while passing an index array or a boolean array or their combination with integer/slice object. Note that advanced indexing always returns a copy of the data (contrast with basic slicing that returns a view).\n",
    "### Integer array indexing\n",
    "Integer array indexing allows selection of arbitrary items in the tensor based on their dimensional index. Indexes passed should be broadcast compatible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a[[0, 1], [1, 2], [1, 0]] = [5 20]\n",
      "Tensor[shape={2}, stride={1}, Int64, CPU:0, 0x55d05ed06570]\n",
      "\n",
      "b = [101 5]\n",
      "Tensor[shape={2}, stride={1}, Int64, CPU:0, 0x55d05ed093e0]\n",
      "\n",
      "a[[0, 0], [0, 1], [1, 1]] = [1 5]\n",
      "Tensor[shape={2}, stride={1}, Int64, CPU:0, 0x55d05758e690]\n"
     ]
    }
   ],
   "source": [
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "\n",
    "# Along each dimension, a specific element is selected.\n",
    "print(\"a[[0, 1], [1, 2], [1, 0]] = {}\\n\".format(a[[0, 1], [1, 2], [1, 0]]))\n",
    "\n",
    "# Changes not reflected as it is a copy.\n",
    "b = a[[0, 0], [0, 1], [1, 1]]\n",
    "b[0] += 100\n",
    "print(\"b = {}\\n\".format(b))\n",
    "print(\"a[[0, 0], [0, 1], [1, 1]] = {}\".format(a[[0, 0], [0, 1], [1, 1]]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Combining advanced and basic indexing\n",
    "When there is at least one slice(```:```), ellipse(```...```), or newaxis in the index, then the behaviour can be more complicated. It is like concatenating the indexing result for each advanced index element. Under the advanced indexing mode, some preprocessing is done before sending to the advanced indexing engine.\n",
    "1. Specific index positions are converted to a Indextensor with the specified index.\n",
    "2. If slice is non-full slice, then we slice the tensor first, then use full slice for advanced indexing engine.\n",
    "\n",
    "```dst = src[1, 0:2, [1, 2]]``` is done in two steps:<br>\n",
    "```temp = src[:, 0:2, :]```<br>\n",
    "```dst = temp[[1], :, [1, 2]]```\n",
    "\n",
    "There are two parts to the indexing operation, the subspace defined by the basic indexing, and the subspace from the advanced indexing part.\n",
    "\n",
    "1. The advanced indexes are separated by a slice, Ellipse, or newaxis. For example ```x[arr1, :, arr2]```.\n",
    "2. The advanced indexes are all next to each other. For example ```x[..., arr1, arr2, :]```, but not ```x[arr1, :, 1]``` since ```1``` is an advanced index here.\n",
    "\n",
    "In the first case, the dimensions resulting from the advanced indexing operation come first in the result array, and the subspace dimensions after that. In the second case, the dimensions from the advanced indexing operations are inserted into the result array at the same spot as they were in the initial array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a[1, 0:2, [1, 2]] = \n",
      "[[13 17],\n",
      " [14 18]]\n",
      "Tensor[shape={2, 2}, stride={2, 1}, Int64, CPU:0, 0x55d05eceb810]\n",
      "\n",
      "a[(0, 1)] = [4 5 6 7]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x55d05ed01170]\n",
      "\n",
      "a[[0, 1] = \n",
      "[[[0 1 2 3],\n",
      "  [4 5 6 7],\n",
      "  [8 9 10 11]],\n",
      " [[12 13 14 15],\n",
      "  [16 17 18 19],\n",
      "  [20 21 22 23]]]\n",
      "Tensor[shape={2, 3, 4}, stride={12, 4, 1}, Int64, CPU:0, 0x55d05ed03150]\n",
      "\n",
      "a[1, [[1, 2], [2, 1]], 0:4:2, [3, 4]] = \n",
      "[[[83 93],\n",
      "  [104 114]],\n",
      " [[103 113],\n",
      "  [84 94]]]\n",
      "Tensor[shape={2, 2, 2}, stride={4, 2, 1}, Int64, CPU:0, 0x55d056caa290]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "vals = np.array(range(24)).reshape((2, 3, 4))\n",
    "a = o3c.Tensor(vals)\n",
    "\n",
    "print(\"a[1, 0:2, [1, 2]] = \\n{}\\n\".format(a[1, 0:2, [1, 2]]))\n",
    "\n",
    "# Subtle difference in selection and advanced indexing.\n",
    "print(\"a[(0, 1)] = {}\\n\".format(a[(0, 1)]))\n",
    "print(\"a[[0, 1] = \\n{}\\n\".format(a[[0, 1]]))\n",
    "\n",
    "a = o3c.Tensor(np.array(range(120)).reshape((2, 3, 4, 5)))\n",
    "\n",
    "# Interleaving slice and advanced indexing.\n",
    "print(\"a[1, [[1, 2], [2, 1]], 0:4:2, [3, 4]] = \\n{}\\n\".format(\n",
    "    a[1, [[1, 2], [2, 1]], 0:4:2, [3, 4]]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Boolean array indexing\n",
    "Advanced indexing gets triggered when we pass a boolean array as an index, or it is returned from comparison operators. Boolean array should have exactly as many dimensions as it is supposed to work with."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a = [1 -1 -2 3]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x55d05eceb810]\n",
      "\n",
      "a = [1 19 18 3]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x55d05eceb810]\n",
      "\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor(np.array([1, -1, -2, 3]))\n",
    "print(\"a = {}\\n\".format(a))\n",
    "\n",
    "# Add constant to all negative numbers.\n",
    "a[a < 0] += 20\n",
    "print(\"a = {}\\n\".format(a))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Logical operations\n",
    "\n",
    "Open3D supports following logical operators:\n",
    "1. `logical_and` - returns tensor with element wise logical AND.\n",
    "2. `logical_or`  - returns tensor with element wise logical OR.\n",
    "3. `logical_xor` - returns tensor with element wise logical XOR.\n",
    "4. `logical_not` - returns tensor with element wise logical NOT.\n",
    "5. `all`         - returns true if all elements in the tensor are true.\n",
    "6. `any`         - returns true if any element in the tensor is true.\n",
    "7. `allclose`    - returns true if two tensors are element wise equal within a tolerance.\n",
    "8. `isclose`     - returns tensor with element wise ```allclose``` operation.\n",
    "9. `issame`      - returns true if and only if two tensors are same(even same underlying memory).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a AND b = [True False False False]\n",
      "Tensor[shape={4}, stride={1}, Bool, CPU:0, 0x55d05757ad90]\n",
      "a OR b = [True True True False]\n",
      "Tensor[shape={4}, stride={1}, Bool, CPU:0, 0x55d05ed0a1a0]\n",
      "a XOR b = [False True True False]\n",
      "Tensor[shape={4}, stride={1}, Bool, CPU:0, 0x55d0bedd9040]\n",
      "NOT a = [False True False True]\n",
      "Tensor[shape={4}, stride={1}, Bool, CPU:0, 0x55d0bf0c0c50]\n",
      "\n",
      "a.any = True\n",
      "a.all = False\n",
      "\n",
      "c AND d = [False False True False]\n",
      "Tensor[shape={4}, stride={1}, Bool, CPU:0, 0x55d0bf0c0c50]\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor(np.array([True, False, True, False]))\n",
    "b = o3c.Tensor(np.array([True, True, False, False]))\n",
    "\n",
    "print(\"a AND b = {}\".format(a.logical_and(b)))\n",
    "print(\"a OR b = {}\".format(a.logical_or(b)))\n",
    "print(\"a XOR b = {}\".format(a.logical_xor(b)))\n",
    "print(\"NOT a = {}\\n\".format(a.logical_not()))\n",
    "\n",
    "# Only works for boolean tensors.\n",
    "print(\"a.any = {}\".format(a.any()))\n",
    "print(\"a.all = {}\\n\".format(a.all()))\n",
    "\n",
    "# If tensor is not boolean, 0 will be treated as False, while non-zero as true.\n",
    "# The tensor will be filled with 0 or 1 casted to tensor's dtype.\n",
    "c = o3c.Tensor(np.array([2.0, 0.0, 3.5, 0.0]))\n",
    "d = o3c.Tensor(np.array([0.0, 3.0, 1.5, 0.0]))\n",
    "print(\"c AND d = {}\".format(c.logical_and(d)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "allclose : True\n",
      "isclose : [True True True True]\n",
      "Tensor[shape={4}, stride={1}, Bool, CPU:0, 0x55d0bedd9040]\n",
      "issame : False\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor(np.array([1, 2, 3, 4]), dtype=o3c.Dtype.Float64)\n",
    "b = o3c.Tensor(np.array([1, 1.99999, 3, 4]))\n",
    "\n",
    "# Throws exception if the device/dtype is not same.\n",
    "# Returns false if the shape is not same.\n",
    "print(\"allclose : {}\".format(a.allclose(b)))\n",
    "\n",
    "# Throws exception if the device/dtype/shape is not same.\n",
    "print(\"isclose : {}\".format(a.isclose(b)))\n",
    "\n",
    "# Returns false if the device/dtype/shape/ is not same.\n",
    "print(\"issame : {}\".format(a.issame(b)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comparison Operations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a > b = [False True False]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d05ed04b10]\n",
      "a >= b = [True True False]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d0a7cdbf60]\n",
      "a < b = [False False True]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d056caa2e0]\n",
      "a <= b = [True False True]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d0565103e0]\n",
      "a == b = [True False False]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d05ed0a1a0]\n",
      "a != b = [False True True]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d0bf3f40e0]\n",
      "a > b = [False True False]\n",
      "Tensor[shape={3}, stride={1}, Bool, CPU:0, 0x55d05ed01130]\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor([0, 1, -1])\n",
    "b = o3c.Tensor([0, 0, 0])\n",
    "\n",
    "print(\"a > b = {}\".format(a > b))\n",
    "print(\"a >= b = {}\".format(a >= b))\n",
    "print(\"a < b = {}\".format(a < b))\n",
    "print(\"a <= b = {}\".format(a <= b))\n",
    "print(\"a == b = {}\".format(a == b))\n",
    "print(\"a != b = {}\".format(a != b))\n",
    "\n",
    "# Throws exception if device/dtype is not shape.\n",
    "# If shape is not same, then tensors should be broadcast compatible.\n",
    "print(\"a > b = {}\".format(a > b[0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Nonzero operations\n",
    "1. When ```as_tuple``` is ```False```(default), it returns a tensor indices of the elements that are non-zero. Each row in the result contains the indices of a non-zero element in the input. If the input has $n$ dimensions, then the resulting tensor is of size $(z x n)$, where $z$ is the total number of non-zero elements in the input tensor.\n",
    "2. When ```as_tuple``` is ```True```, it returns a tuple of 1D tensors, one for each dimension in input, each containing the indices of all non-zero elements of input. If the input has $n$ dimension, then the resulting tuple contains $n$ tensors of size $z$, where $z$ is the total number of non-zero elements in the input tensor.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a = \n",
      "[[3 0 0],\n",
      " [0 4 0],\n",
      " [5 6 0]]\n",
      "Tensor[shape={3, 3}, stride={3, 1}, Int64, CPU:0, 0x55d056510470]\n",
      "\n",
      "a.nonzero() = \n",
      "[[0 1 2 2]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x55d05ed0a290], [0 1 0 1]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x55d0bf3f4090]]\n",
      "\n",
      "a.nonzero(as_tuple = 1) = \n",
      "[[0 1 2 2],\n",
      " [0 1 0 1]]\n",
      "Tensor[shape={2, 4}, stride={4, 1}, Int64, CPU:0, 0x55d05758e690]\n"
     ]
    }
   ],
   "source": [
    "a = o3c.Tensor([[3, 0, 0], [0, 4, 0], [5, 6, 0]])\n",
    "\n",
    "print(\"a = \\n{}\\n\".format(a))\n",
    "print(\"a.nonzero() = \\n{}\\n\".format(a.nonzero()))\n",
    "print(\"a.nonzero(as_tuple = 1) = \\n{}\".format(a.nonzero(as_tuple=1)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pickle support \n",
    "Since Open3D v0.16.0, tensor can be serialized and deserialized using pickle. This is useful for saving and loading tensors to/from disk."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "After serialization: [1 2 3 4]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x559b079046e0]\n",
      "\n",
      "After deserialization: [1 2 3 4]\n",
      "Tensor[shape={4}, stride={1}, Int64, CPU:0, 0x559b06ba7fc0]\n",
      "\n",
      "After serialization: [1 2 3 4]\n",
      "Tensor[shape={4}, stride={1}, Int64, CUDA:0, 0x302000000]\n",
      "\n",
      "After deserialization: [1 2 3 4]\n",
      "Tensor[shape={4}, stride={1}, Int64, CUDA:0, 0x302000200]\n",
      "\n",
      "Contiguous: False\n",
      "\n",
      "Contiguous: True\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "import pickle\n",
    "import tempfile\n",
    "\n",
    "\n",
    "a = o3c.Tensor([1, 2, 3, 4])\n",
    "print(f'After serialization: {a}\\n')\n",
    "with tempfile.TemporaryDirectory() as path:\n",
    "    file_name = os.path.join(path, 'tensor')\n",
    "    pickle.dump(a, open(file_name, 'wb'))\n",
    "    b = pickle.load(open(file_name, 'rb'))\n",
    "    print(f'After deserialization: {b}\\n')\n",
    "\n",
    "# Pickle tensor on GPU.\n",
    "a = o3c.Tensor([1, 2, 3, 4], device=o3c.Device('cuda:0'))\n",
    "print(f'After serialization: {a}\\n')\n",
    "with tempfile.TemporaryDirectory() as path:\n",
    "    file_name = os.path.join(path, 'tensor')\n",
    "    pickle.dump(a, open(file_name, 'wb'))\n",
    "    b = pickle.load(open(file_name, 'rb'))\n",
    "    print(f'After deserialization: {b}\\n')\n",
    "\n",
    "# Pickle non-contiguous tensor.\n",
    "a = o3c.Tensor.ones((100))\n",
    "a = a[::2]\n",
    "print(f'Contiguous: {a.is_contiguous()}\\n')\n",
    "with tempfile.TemporaryDirectory() as path:\n",
    "    file_name = os.path.join(path, 'tensor')\n",
    "    pickle.dump(a, open(file_name, 'wb'))\n",
    "    b = pickle.load(open(file_name, 'rb'))\n",
    "    print(f'Contiguous: {b.is_contiguous()}\\n')"
   ]
  }
 ],
 "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.8.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
