diff --git a/_toc.yml b/_toc.yml index b468b3a..3aefc32 100644 --- a/_toc.yml +++ b/_toc.yml @@ -7,4 +7,5 @@ parts: sections: - file: notebooks/intro-mpi/send-recv - file: notebooks/intro-mpi/scatter-gather - - file: notebooks/send-vs-send \ No newline at end of file + - file: notebooks/send-vs-send + - file: notebooks/dolfinx_MPI_tutorial \ No newline at end of file diff --git a/notebooks/dolfinx_MPI_tutorial.ipynb b/notebooks/dolfinx_MPI_tutorial.ipynb new file mode 100644 index 0000000..187eba3 --- /dev/null +++ b/notebooks/dolfinx_MPI_tutorial.ipynb @@ -0,0 +1,896 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d29c16a2", + "metadata": {}, + "source": [ + "# Parallel Computations with Dolfinx using MPI\n", + "\n", + "The aim of this tutorial is to show how variational problems can be solved with Python using Dolfinx running in parallel. The Message Passing Interface (MPI) standard will be used to carry out parallel computations. We will use the mpi4py package to interface MPI in Python.\n", + "\n", + "First, we will look at some basic examples of MPI usage.\n", + "\n", + "Next, we will cover how to define finite element function spaces and functions on several processes.\n", + "\n", + "Furthermore, creating and distributing a finite element mesh in parallel will be demonstrated.\n", + "\n", + "Finally, the elements of the tutorial are combined to show how the variational problem related to a partial differential equation can be solved in parallel.\n", + "\n", + "This tutorial is inspired by and based on https://newfrac.gitlab.io/newfrac-fenicsx-training/05-dolfinx-parallel/dolfinx-parallel.html and https://jsdokken.com/dolfinx_docs/meshes.html.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a6f541fd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "## Parallel programming imports\n", + "import ipyparallel as ipp\n", + "\n", + "from mpi4py import MPI" + ] + }, + { + "cell_type": "markdown", + "id": "bcfffafd-83ee-4d09-a2db-303070669b0a", + "metadata": {}, + "source": [ + "## Setting up a cluster\n", + "ipyparallel is used to set up a local cluster consisting of 2 processors. To run Jupyter Notebook cells in parallel, we use %%px cell magic. To learn more about this, see the first parts of the MPI tutorial ([Introduction to MPI](./intro-mpi/intro-mpi.ipynb)) as well as https://ipyparallel.readthedocs.io/en/latest/tutorial/magics.html#px-cell-magic." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3c140fdc", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting 2 engines with \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/anaconda3/envs/fenicsx/lib/python3.10/site-packages/ipyparallel/util.py:210: RuntimeWarning: IPython could not determine IPs for Halvors-MacBook-Pro-2.local: [Errno 1] Unknown host\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "af2a1e19d5c4449e9b1ebf4c34c4609b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/2 [00:00 with 6 nodes\n", + " 0: [0 8 7 ]\n", + " 1: [2 1 0 ]\n", + " 2: [4 1 3 ]\n", + " 3: [6 5 2 ]\n", + " 4: [9 8 10 ]\n", + " 5: [12 5 11 ]\n", + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:0] Cell (dim = 2) to facet (dim = 1) connectivity:\n", + "Rank 0: with 6 nodes\n", + " 0: [5 4 0 ]\n", + " 1: [6 1 5 ]\n", + " 2: [3 1 2 ]\n", + " 3: [7 8 6 ]\n", + " 4: [10 4 9 ]\n", + " 5: [12 8 11 ]\n", + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "mesh.topology.create_connectivity(2, 1)\n", + "print(\"Cell (dim = 2) to facet (dim = 1) connectivity:\")\n", + "mpi_print(mesh.topology.connectivity(2, 1))" + ] + }, + { + "cell_type": "markdown", + "id": "f6f89363-c8f9-4f02-a634-d9d69b087565", + "metadata": {}, + "source": [ + "The ghost nodes for each processor rank is stored in the index map of the mesh topology:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8d8e71e5-ba72-4aa3-83df-24692e01cab6", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:0] Rank 0: Ghost cells (global numbering): [4 7]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:1] Rank 1: Ghost cells (global numbering): [0 3]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "mpi_print(f\"Ghost cells (global numbering): {mesh.topology.index_map(2).ghosts}\")" + ] + }, + { + "cell_type": "markdown", + "id": "90adf486-bcba-4259-b93f-77080d9cb6cc", + "metadata": { + "tags": [] + }, + "source": [ + "## Dolfinx function spaces\n", + "The degrees of freedom of a finite element function space in dolfinx is distributed over the nodes of the mesh. To illustrate, we create a function space with 1st order Lagrange elements and print the global and local sizes of the dofmap, as well as the ghost nodes." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "ca8b487d-09e4-45c4-bd90-784f511f74d3", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:1] Rank 1: Global dofmap size: 9\n", + "Rank 1: Local dofmap size: 5\n", + "Rank 1: Ghosts: [0 1 2]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:0] Rank 0: Global dofmap size: 9\n", + "Rank 0: Local dofmap size: 4\n", + "Rank 0: Ghosts: [5 8 4 6]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "V = dfx.fem.FunctionSpace(mesh, (\"Lagrange\", 1))\n", + "\n", + "mpi_print(f\"Global dofmap size: {V.dofmap.index_map.size_global}\")\n", + "mpi_print(f\"Local dofmap size: {V.dofmap.index_map.size_local}\")\n", + "mpi_print(f\"Ghosts: {V.dofmap.index_map.ghosts}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e2a6141a-5fb2-44c8-a8c9-2f1b035039e4", + "metadata": {}, + "source": [ + "## Dolfinx functions\n", + "The degrees of freedom of a dolfinx function is distributed over the nodes of the mesh in the same way as function spaces, as the functions created from a function space inherit the dofmap of the space that they live in. We create a function from the previously defined space $V$ and print the size of the array." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "8fa7a806-d9ee-405c-965e-6fec591e028c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:1] Rank 1: Local size of array: 5\n", + "Rank 1: Global size of array: 9\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:0] Rank 0: Local size of array: 4\n", + "Rank 0: Global size of array: 9\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "u = dfx.fem.Function(V)\n", + "mpi_print(f\"Local size of array: {u.x.map.size_local}\")\n", + "mpi_print(f\"Global size of array: {u.x.map.size_global}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0a3adac5-7d4b-48f8-9254-16ab700310db", + "metadata": {}, + "source": [ + "Since we have a scalar function, the size of the array of the function values is the same as the number of nodes in the mesh. If we e.g. had a two-dimensional vector function, the size of the array would be double the amount of mesh nodes.\n", + "\n", + "We can also print the ghost nodes and the rank of the processor owning the ghost nodes:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0699659e-9d73-48d2-a0be-36f3288dedea", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:0] Rank 0: Ghosts: [5 8 4 6]\n", + "Rank 0: Ghost owners: [1 1 1 1]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:1] Rank 1: Ghosts: [0 1 2]\n", + "Rank 1: Ghost owners: [0 0 0]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "mpi_print(f\"Ghosts: {u.x.map.ghosts}\")\n", + "mpi_print(f\"Ghost owners: {u.x.map.owners}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0f671131-50fe-4aeb-bb00-1454fd3de745", + "metadata": {}, + "source": [ + "## Assembling scalars, vectors, matrices in parallel\n", + "To solve continuous problems numerically, we have to assemble a linear system of equations arising from discretization. Assembling scalars, vectors and matrices in dolfinx has to be carried out carefully when using several processes. We have to make sure that the processors communicate changes in values of overlapping nodes. We start by creating trial and test functions $u$ and $v$ from our function space $V$." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "230c3bb9-3b52-4450-870a-f2d8541b6457", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%px\n", + "\n", + "# Trial and test functions\n", + "u = ufl.TrialFunction(V)\n", + "v = ufl.TestFunction (V)" + ] + }, + { + "cell_type": "markdown", + "id": "f91f8a76-994c-4c16-9c67-21f9790b73d0", + "metadata": {}, + "source": [ + "Let us consider a linear form\n", + "$$L(v) = \\int_{\\Omega}f v dx $$\n", + "where $v$ is a test function, $\\Omega$ is the domain that we have discretized with our mesh and $f$ is a scalar-valued function. The test function is discretized with 1st order continuous Lagrange elements, and to assemble it as a vector we can run" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "3f8dbf3b-12ac-428e-a078-906277074abc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%px\n", + "\n", + "import ufl\n", + "\n", + "# UFL form of right-hand side\n", + "L = ufl.inner(1.0, v) * ufl.dx\n", + "L = dfx.fem.form(L)\n", + "\n", + "# Assemble UFL form into a vector\n", + "_b = dfx.fem.Function(V)\n", + "dfx.fem.petsc.assemble_vector(_b.vector, L)\n", + "_b.x.scatter_forward()\n" + ] + }, + { + "cell_type": "markdown", + "id": "f786bfb9-109d-489e-8dba-5bac4638e465", + "metadata": {}, + "source": [ + "Now, after assembling, it is important to distribute the node values from the different processes. After the initial assembly, our vector holds the values" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "2a4ebf6e-f31e-46b5-8ee6-e168df63c3f8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:1] Prior to communication\n", + "Rank 1: [0.125 0.125 0.125 0.04166667 0.04166667 0.04166667\n", + " 0.125 0.125 ]\n", + "After ADD/REVERSE update\n", + "Rank: 1: [0.25 0.25 0.25 0.04166667 0.08333333 0.04166667\n", + " 0.125 0.125 ]\n", + "After INSERT/FORWARD update\n", + "Rank: 1: [0.25 0.25 0.25 0.04166667 0.08333333 0.08333333\n", + " 0.25 0.25 ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:0] Prior to communication\n", + "Rank 0: [0.04166667 0.125 0.125 0.04166667 0.125 0.04166667\n", + " 0.125 0.125 ]\n", + "After ADD/REVERSE update\n", + "Rank: 0: [0.08333333 0.25 0.25 0.04166667 0.125 0.04166667\n", + " 0.125 0.125 ]\n", + "After INSERT/FORWARD update\n", + "Rank: 0: [0.08333333 0.25 0.25 0.04166667 0.25 0.08333333\n", + " 0.25 0.25 ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "# Print the size of the index map and the number of ghost nodes\n", + "#print(V.dofmap.index_map.size_local*V.dofmap.index_map_bs, V.dofmap.index_map.num_ghosts*V.dofmap.index_map_bs)\n", + "print(\"Prior to communication\")\n", + "mpi_print(_b.x.array) \n", + "\n", + "# Add values from ghost regions and accumulate them on the owning process\n", + "_b.x.scatter_reverse(dfx.la.ScatterMode.add)\n", + "\n", + "#_b.vector.ghostUpdate(addv = PETSc.InsertMode.ADD, mode = PETSc.ScatterMode.REVERSE)\n", + "\n", + "print(\"After ADD/REVERSE update\")\n", + "print(f\"Rank: {comm.rank}: {_b.x.array}\") \n", + "\n", + "# Ghost points still not updated, so their values are inconsistent\n", + "# Get value from owning process and update the ghosts\n", + "_b.x.scatter_forward()\n", + "#_b.ghostUpdate(addv = PETSc.InsertMode.INSERT, mode = PETSc.ScatterMode.FORWARD)\n", + "\n", + "print(\"After INSERT/FORWARD update\")\n", + "print(f\"Rank: {comm.rank}: {_b.x.array}\") \n" + ] + }, + { + "cell_type": "markdown", + "id": "b150a3dd-a283-4230-b9a2-f6309836ecd4", + "metadata": {}, + "source": [ + "The values from the ghost nodes can be accumulated on the owning process by running" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "e8e1b2fe-944b-4944-970d-908ec9ab068b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:1] After ADD/REVERSE update\n", + "Rank 1: [0.5 0.5 0.5 0.04166667 0.16666667 0.08333333\n", + " 0.25 0.25 ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:0] After ADD/REVERSE update\n", + "Rank 0: [0.16666667 0.5 0.5 0.04166667 0.25 0.08333333\n", + " 0.25 0.25 ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "\n", + "# Add values from ghost regions and accumulate them on the owning process\n", + "_b.x.scatter_reverse(dfx.la.ScatterMode.add)\n", + "\n", + "print(\"After ADD/REVERSE update\")\n", + "mpi_print(_b.x.array) " + ] + }, + { + "cell_type": "markdown", + "id": "ef61475c-bd55-4852-9182-40eaeb27db99", + "metadata": {}, + "source": [ + "The ghost nodes are still not updated, and now we must distribute the values from the owning process to the ghost nodes" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "6f7059a8-e73c-49d7-8b0c-417f80a8f065", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:0] After INSERT/FORWARD update\n", + "Rank 0: [0.16666667 0.5 0.5 0.04166667 0.5 0.16666667\n", + " 0.5 0.5 ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:1] After INSERT/FORWARD update\n", + "Rank 1: [0.5 0.5 0.5 0.04166667 0.16666667 0.16666667\n", + " 0.5 0.5 ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%px\n", + "\n", + "# Get value from owning process and update the ghosts\n", + "_b.x.scatter_forward()\n", + "\n", + "print(\"After INSERT/FORWARD update\")\n", + "mpi_print(_b.x.array)" + ] + }, + { + "cell_type": "markdown", + "id": "8d082121-d6c1-4e77-8e6d-58bfd2817fe9", + "metadata": {}, + "source": [ + "## Putting it all together: Solving a variational problem\n", + "We will consider solving a Poisson problem on the unit square domain, denoted $\\Omega$. The strong form of the problem is: determine $u$ such that\n", + "\\begin{align}\n", + " -\\nabla^2 u &= f \\quad \\mathrm{in} \\ \\Omega, \\\\\n", + " u &= g \\quad \\mathrm{on} \\ \\partial\\Omega,\n", + "\\end{align}\n", + "where $\\partial\\Omega$ is the boundary of the domain. The weak form of the problem is derived by multiplying the PDE with a test function $v$, integrating over the domain and applying integration by parts. This yields\n", + "\n", + "$$\\int_{\\Omega} \\nabla u \\cdot \\nabla v dx = \\int_{\\Omega}f v dx$$\n", + "\n", + "where the boundary integral vanishes because $v = 0$ on the boundary due to the Dirichlet boundary condition. For simplicity we set $g = 0$.\n", + "\n", + "The finite element problem can now be defined as: find $u_h \\in V_h$ such that\n", + "$$a(u_h, v_h) = L(v_h), \\forall \\ v_h \\in V_h,$$\n", + "where $V_h$ is the finite element space and\n", + "$$a(u, v) = \\int_{\\Omega} \\nabla u \\cdot \\nabla v dx$$\n", + "and\n", + "$$L(v) = \\int_{\\Omega}f v dx.$$\n", + "The subscript $h$ emphasizes that the variables are defined on a discrete mesh.\n", + "\n", + "PETSc is the linear algebra backend used for solving the linear system of equations that defines the weak form. For more information on the Krylov solver used here and its options, see: https://petsc.org/release/.\n", + "\n", + "To visualize the solution, we use pyvista (https://docs.pyvista.org/). For a simple introduction to defining and solving variational problems with FEniCSx, see https://jsdokken.com/dolfinx-tutorial/." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "1bd0b245-b1e4-4d1f-9e11-a12e652a4bcb", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[stdout:1] 5 1\n", + "Prior to communication\n", + "Rank: 1: [0. 0.0625 0. 0. 0. 0. ]\n", + "After scatter_forward update\n", + "Rank: 1: [0. 0.0625 0. 0. 0. 0. ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[stdout:0] 4 2\n", + "Prior to communication\n", + "Rank: 0: [0. 0. 0. 0. 0. 0.]\n", + "After scatter_forward update\n", + "Rank: 0: [0. 0. 0. 0. 0.0625 0. ]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1e2971e92d294c99adf19eaa643ce5cb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "%px: 0%| | 0/2 [00:00 2\u001b[0m \u001b[43mrc\u001b[49m\u001b[38;5;241m.\u001b[39mcluster\u001b[38;5;241m.\u001b[39mstop_cluster_sync()\n", + "\u001b[0;31mNameError\u001b[0m: name 'rc' is not defined" + ] + } + ], + "source": [ + "# Stop the cluster\n", + "rc.cluster.stop_cluster_sync()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7d00d30-187b-4e6d-801a-2fc663de7425", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/send-vs-send.ipynb b/notebooks/send-vs-send.ipynb index f483ac7..f428f54 100644 --- a/notebooks/send-vs-send.ipynb +++ b/notebooks/send-vs-send.ipynb @@ -24,7 +24,9 @@ "cell_type": "code", "execution_count": 1, "id": "cb38b8dd-07b7-4f10-8083-78b61b932e51", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import logging\n", @@ -53,7 +55,7 @@ { "data": { "text/plain": [ - "[stdout:1] I am rank 1 / 2\n" + "[stdout:0] I am rank 0 / 2\n" ] }, "metadata": {}, @@ -62,7 +64,7 @@ { "data": { "text/plain": [ - "[stdout:0] I am rank 0 / 2\n" + "[stdout:1] I am rank 1 / 2\n" ] }, "metadata": {}, @@ -228,10 +230,10 @@ { "data": { "text/plain": [ - "[stdout:1] send\n", - "358 µs ± 7.58 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n", + "[stdout:0] send\n", + "339 µs ± 16.2 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n", "Send\n", - "63 µs ± 894 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" + "55.5 µs ± 646 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" ] }, "metadata": {}, @@ -240,10 +242,10 @@ { "data": { "text/plain": [ - "[stdout:0] send\n", - "358 µs ± 7.58 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n", + "[stdout:1] send\n", + "339 µs ± 16.1 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n", "Send\n", - "63 µs ± 894 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" + "55.5 µs ± 646 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" ] }, "metadata": {}, @@ -252,7 +254,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2c29a3b3c6b04d008b081b0b48119101", + "model_id": "96bac589e7224069b0f6c1c3c042c619", "version_major": 2, "version_minor": 0 }, @@ -295,7 +297,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ea1a578b92e04a919bd0d79eb4ae67d7", + "model_id": "5e88f962f09c46948738b4c0e25c41ac", "version_major": 2, "version_minor": 0 }, @@ -331,7 +333,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "8c7a5a5b-bfab-4bd8-a93b-6997761aab24", "metadata": { "tags": [] @@ -367,7 +369,7 @@ " \n", " 0\n", " send\n", - " 0.000023\n", + " 0.000020\n", " 1000\n", " \n", " \n", @@ -379,7 +381,7 @@ " \n", " 2\n", " send\n", - " 0.000025\n", + " 0.000022\n", " 1412\n", " \n", " \n", @@ -391,7 +393,7 @@ " \n", " 4\n", " send\n", - " 0.000029\n", + " 0.000023\n", " 1995\n", " \n", " \n", @@ -400,14 +402,14 @@ ], "text/plain": [ " kind per_call size\n", - "0 send 0.000023 1000\n", + "0 send 0.000020 1000\n", "1 Send 0.000003 1000\n", - "2 send 0.000025 1412\n", + "2 send 0.000022 1412\n", "3 Send 0.000003 1412\n", - "4 send 0.000029 1995" + "4 send 0.000023 1995" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -430,7 +432,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "7191a56d-6048-4e21-9ef7-94778bc14484", "metadata": { "tags": [] @@ -441,19 +443,19 @@ "text/plain": [ "size kind\n", "1000 Send 0.000003\n", - " send 0.000023\n", + " send 0.000020\n", "1412 Send 0.000003\n", - " send 0.000025\n", - "1995 Send 0.000004\n", - " send 0.000029\n", + " send 0.000022\n", + "1995 Send 0.000003\n", + " send 0.000023\n", "2818 Send 0.000004\n", - " send 0.000033\n", + " send 0.000035\n", "3981 Send 0.000005\n", - " send 0.000043\n", + " send 0.000036\n", "Name: per_call, dtype: float64" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -472,7 +474,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "id": "a331197b-490d-41e2-8240-1d8768d27268", "metadata": { "tags": [] @@ -482,13 +484,13 @@ "data": { "text/html": [ "\n", - "
\n", + "
\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -564,7 +566,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "id": "aaeef9b8-5145-4f65-b6f3-5797665d79d0", "metadata": { "tags": [] @@ -574,13 +576,13 @@ "data": { "text/html": [ "\n", - "
\n", + "
\n", "" ], "text/plain": [ "alt.Chart(...)" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -658,7 +660,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "id": "1ef0812d-9661-40f2-8858-b1d0c3eba578", "metadata": { "tags": [] @@ -667,6 +669,14 @@ "source": [ "rc.cluster.stop_cluster_sync()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6cccb55-d26d-4e20-8402-346394e9b9e5", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -685,7 +695,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.10.7" }, "widgets": { "application/vnd.jupyter.widget-state+json": {