{ "cells": [ { "attachments": {}, "cell_type": "markdown", "id": "ffabb623", "metadata": { "tags": [] }, "source": [ "# Getting Started - Matrix Multiplication\n", "\n", "In this tutorial, you will learn how to:\n", "\n", "1. Install `egglog` Python\n", "2. Create a representation for matrices and some simplification rules for them. This will be based off of the [matrix multiplication example](https://github.com/egraphs-good/egglog/blob/08a6e8f/tests/matrix.egg) in the egglog repository. By using our high level wrapper, we can rely on Python's built in static type checker to check the correctness of your representation.\n", "3. Try out using our library in an interactive notebook.\n", "\n", "## Install egglog Python\n", "\n", "First, you will need to have a working Python interpreter. In this tutorial, we will [use `miniconda`](https://docs.conda.io/en/latest/miniconda.html) to create a new Python environment and activate it:\n", "\n", "```bash\n", "$ brew install miniconda\n", "$ conda create -n egglog-python python=3.11\n", "$ conda activate egglog-python\n", "```\n", "\n", "Then we want to install `egglog` Python. `egglog` Python can run on any recent Python version, and is tested on 3.8 - 3.11. To install it, run:\n", "\n", "```bash\n", "$ pip install egglog\n", "```\n", "\n", "To test you have installed it correctly, run:\n", "\n", "```bash\n", "$ python -m 'import egglog'\n", "```\n", "\n", "We also want to install `mypy` for static type checking. This is not required, but it will help us write correct representations. To install it, run:\n", "\n", "```bash\n", "$ pip install mypy\n", "```\n", "\n", "## Creating an E-Graph\n", "\n", "In this tutorial, we will use [VS Code](https://code.visualstudio.com/) to create file, `matrix.py`, to include our egraph\n", "and the simplification rules:\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "7369b71b", "metadata": {}, "outputs": [], "source": [ "from __future__ import annotations\n", "\n", "from egglog import *\n", "\n", "egraph = EGraph()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "814a51c5", "metadata": {}, "source": [ "## Defining Dimensions\n", "\n", "We will start by defining a representation for integers, which we will use to represent\n", "the dimensions of the matrix:\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "04fa991a", "metadata": {}, "outputs": [], "source": [ "@egraph.class_\n", "class Dim(Expr):\n", " \"\"\"\n", " A dimension of a matix.\n", "\n", " >>> Dim(3) * Dim.named(\"n\")\n", " Dim(3) * Dim.named(\"n\")\n", " \"\"\"\n", "\n", " def __init__(self, value: i64Like) -> None:\n", " ...\n", "\n", " @classmethod\n", " def named(cls, name: StringLike) -> Dim:\n", " ...\n", "\n", " def __mul__(self, other: Dim) -> Dim:\n", " ..." ] }, { "attachments": {}, "cell_type": "markdown", "id": "f5098a2b", "metadata": { "tags": [] }, "source": [ "As you can see, you must wrap any class with the `egraph.class_` to register\n", "it with the egraph and be able to use it like a Python class.\n", "\n", "### Testing in a notebook\n", "\n", "We can try out this by [creating a new notebook](https://code.visualstudio.com/docs/datascience/jupyter-notebooks#_create-or-open-a-jupyter-notebook) which imports this file:\n", "\n", "```python\n", "from matrix import *\n", "```\n" ] }, { "attachments": {}, "cell_type": "markdown", "id": "fd43c7ef", "metadata": {}, "source": [ "We can then create a new `Dim` object:\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "b6424530", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
(Dim.named("x") * Dim(10)) * Dim(10)\n",
       "
\n" ], "text/latex": [ "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", "\\PY{p}{(}\\PY{n}{Dim}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{x}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)} \\PY{o}{*} \\PY{n}{Dim}\\PY{p}{(}\\PY{l+m+mi}{10}\\PY{p}{)}\\PY{p}{)} \\PY{o}{*} \\PY{n}{Dim}\\PY{p}{(}\\PY{l+m+mi}{10}\\PY{p}{)}\n", "\\end{Verbatim}\n" ], "text/plain": [ "(Dim.named(\"x\") * Dim(10)) * Dim(10)" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "x = Dim.named(\"x\")\n", "ten = Dim(10)\n", "res = x * ten * ten\n", "res" ] }, { "attachments": {}, "cell_type": "markdown", "id": "ef5ebb16", "metadata": {}, "source": [ "We see that the output is not evaluated, it's just a representation of the computation as well as the type. This is because we haven't defined any simplification rules yet.\n", "\n", "We can also try to create a dimension from an invalid type, or use it in an invalid way, we get a type error before we even run the code:\n", "\n", "```python\n", "x - ten\n", "```\n", "\n", "![Screenshot of VS Code showing a type error](./screenshot-1.png)\n", "\n", "## Dimension Replacements\n", "\n", "Now we will register some replacements for our dimensions and see how we can interface with egg to get it\n", "to execute them.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "b06b1749", "metadata": {}, "outputs": [], "source": [ "a, b, c = vars_(\"a b c\", Dim)\n", "i, j = vars_(\"i j\", i64)\n", "egraph.register(\n", " rewrite(a * (b * c)).to((a * b) * c),\n", " rewrite((a * b) * c).to(a * (b * c)),\n", " rewrite(Dim(i) * Dim(j)).to(Dim(i * j)),\n", " rewrite(a * b).to(b * a),\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "id": "167722d1-60b8-452a-ae54-6a8df4db5b00", "metadata": {}, "source": [ "You might notice that unlike a traditional term rewriting system, we don't specify any order for these rewrites. They will be executed until the graph is fully saturated, meaning that no new terms are created.\n" ] }, { "attachments": {}, "cell_type": "markdown", "id": "a4d2c911", "metadata": {}, "source": [ "We can also see how the type checking can help us. If we try to create a rewrite from a `Dim` to an `i64` we see that we get a type error:\n", "\n", "![Screenshot of VS Code showing a type error](./screenshot-2.png)\n" ] }, { "attachments": {}, "cell_type": "markdown", "id": "76dc1672-dba6-44ab-b9f1-aa01de685fb1", "metadata": {}, "source": [ "### Testing\n", "\n", "Going back to the notebook, we can test out the that the rewrites are working.\n", "We can run some number of iterations and extract out the lowest cost expression which is equivalent to our variable:\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "31afa12e-da68-4398-91fa-14523f6c099a", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/html": [ "
Dim.named("x") * Dim(100)\n",
       "
\n" ], "text/latex": [ "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", "\\PY{n}{Dim}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{x}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)} \\PY{o}{*} \\PY{n}{Dim}\\PY{p}{(}\\PY{l+m+mi}{100}\\PY{p}{)}\n", "\\end{Verbatim}\n" ], "text/plain": [ "Dim.named(\"x\") * Dim(100)" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "egraph.simplify(res, 10)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "7e44104c-d87b-441d-a717-92d42aab9d37", "metadata": {}, "source": [ "## Matrix Expressions\n", "\n", "Now that we have defined dimensions, we can define matrices as well as some functions on them:\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "c5b96cfb", "metadata": {}, "outputs": [], "source": [ "@egraph.class_\n", "class Matrix(Expr):\n", " @classmethod\n", " def identity(cls, dim: Dim) -> Matrix:\n", " \"\"\"\n", " Create an identity matrix of the given dimension.\n", " \"\"\"\n", " ...\n", "\n", " @classmethod\n", " def named(cls, name: StringLike) -> Matrix:\n", " \"\"\"\n", " Create a named matrix.\n", " \"\"\"\n", " ...\n", "\n", " def __matmul__(self, other: Matrix) -> Matrix:\n", " \"\"\"\n", " Matrix multiplication.\n", " \"\"\"\n", " ...\n", "\n", " def nrows(self) -> Dim:\n", " \"\"\"\n", " Number of rows in the matrix.\n", " \"\"\"\n", " ...\n", "\n", " def ncols(self) -> Dim:\n", " \"\"\"\n", " Number of columns in the matrix.\n", " \"\"\"\n", " ...\n", "\n", "\n", "@egraph.function\n", "def kron(a: Matrix, b: Matrix) -> Matrix:\n", " \"\"\"\n", " Kronecker product of two matrices.\n", "\n", " https://en.wikipedia.org/wiki/Kronecker_product#Definition\n", " \"\"\"\n", " ..." ] }, { "attachments": {}, "cell_type": "markdown", "id": "be8e6526", "metadata": {}, "source": [ "### Rows/cols Replacements\n", "\n", "We can also define some replacements to understand the number of rows and columns of a matrix:\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "cb2b4fb8", "metadata": {}, "outputs": [], "source": [ "A, B, C, D = vars_(\"A B C D\", Matrix)\n", "egraph.register(\n", " # The dimensions of a kronecker product are the product of the dimensions\n", " rewrite(kron(A, B).nrows()).to(A.nrows() * B.nrows()),\n", " rewrite(kron(A, B).ncols()).to(A.ncols() * B.ncols()),\n", " # The dimensions of a matrix multiplication are the number of rows of the first\n", " # matrix and the number of columns of the second matrix.\n", " rewrite((A @ B).nrows()).to(A.nrows()),\n", " rewrite((A @ B).ncols()).to(B.ncols()),\n", " # The dimensions of an identity matrix are the input dimension\n", " rewrite(Matrix.identity(a).nrows()).to(a),\n", " rewrite(Matrix.identity(a).ncols()).to(a),\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "id": "13b969e8", "metadata": {}, "source": [ "We can try these out in our notebook (after restarting and re-importing) to compute the dimensions after some operations:\n" ] }, { "cell_type": "code", "execution_count": 8, "id": "8d18be2d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dim.named(\"y\")\n", "Dim.named(\"x\")\n" ] } ], "source": [ "# If we multiply two identity matrices, we should be able to get the number of columns of the result\n", "x = Matrix.identity(Dim.named(\"x\"))\n", "y = Matrix.identity(Dim.named(\"y\"))\n", "x_mult_y = x @ y\n", "print(egraph.simplify(x_mult_y.ncols(), 10))\n", "print(egraph.simplify(x_mult_y.nrows(), 10))" ] }, { "attachments": {}, "cell_type": "markdown", "id": "2f2c68c3", "metadata": {}, "source": [ "### Operation replacements\n", "\n", "We can also define some replacements for matrix operations:\n" ] }, { "cell_type": "code", "execution_count": 9, "id": "18a91684", "metadata": {}, "outputs": [], "source": [ "egraph.register(\n", " # Multiplication by an identity matrix is the same as the other matrix\n", " rewrite(A @ Matrix.identity(a)).to(A),\n", " rewrite(Matrix.identity(a) @ A).to(A),\n", " # Matrix multiplication is associative\n", " rewrite((A @ B) @ C).to(A @ (B @ C)),\n", " rewrite(A @ (B @ C)).to((A @ B) @ C),\n", " # Kronecker product is associative\n", " rewrite(kron(A, kron(B, C))).to(kron(kron(A, B), C)),\n", " rewrite(kron(kron(A, B), C)).to(kron(A, kron(B, C))),\n", " # Kronecker product distributes over matrix multiplication\n", " rewrite(kron(A @ C, B @ D)).to(kron(A, B) @ kron(C, D)),\n", " rewrite(kron(A, B) @ kron(C, D)).to(\n", " kron(A @ C, B @ D),\n", " # Only when the dimensions match\n", " eq(A.ncols()).to(C.nrows()),\n", " eq(B.ncols()).to(D.nrows()),\n", " ),\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "id": "1cd649dc", "metadata": {}, "source": [ "In our previous tests, we had to add the `ncols` and `nrows` operations to the e-graph seperately in order to have them be simplified. We can write some \"demand\" rules which automatically add these operations to the e-graph when they are needed:\n" ] }, { "cell_type": "code", "execution_count": 10, "id": "303ce7f3", "metadata": {}, "outputs": [], "source": [ "egraph.register(\n", " # demand rows and columns when we multiply matrices\n", " rule(A @ B).then(\n", " A.ncols(),\n", " A.nrows(),\n", " B.nrows(),\n", " B.ncols(),\n", " ),\n", " # demand rows and columns when we take the kronecker product\n", " rule(kron(A, B)).then(\n", " A.ncols(),\n", " A.nrows(),\n", " B.nrows(),\n", " B.ncols(),\n", " ),\n", ")" ] }, { "cell_type": "markdown", "id": "334a2cc4-0004-415a-a8fb-4c5ef2e26aec", "metadata": {}, "source": [ "For example, if we have `X @ Y` in the egraph, it will add expression for the columns of each as well:" ] }, { "cell_type": "code", "execution_count": 11, "id": "c79a9105-e8fe-4545-b7c6-262648f82aad", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "outer_cluster_1\n", "\n", "\n", "cluster_1\n", "\n", "\n", "\n", "outer_cluster_0\n", "\n", "\n", "cluster_0\n", "\n", "\n", "\n", "outer_cluster_2\n", "\n", "\n", "cluster_2\n", "\n", "\n", "\n", "outer_cluster_String-1316606400713378063\n", "\n", "\n", "cluster_String-1316606400713378063\n", "\n", "\n", "\n", "outer_cluster_String-4801791173778264996\n", "\n", "\n", "cluster_String-4801791173778264996\n", "\n", "\n", "\n", "\n", "Matrix_named-1316606400713378063:s->String-1316606400713378063\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-4801791173778264996:s->String-4801791173778264996\n", "\n", "\n", "\n", "\n", "\n", "Matrix___matmul__-5871781006564002453:s->Matrix_named-1316606400713378063\n", "\n", "\n", "\n", "\n", "\n", "Matrix___matmul__-5871781006564002453:s->Matrix_named-4801791173778264996\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-1316606400713378063\n", "\n", "\n", "Matrix_named\n", "\n", "\n", "\n", "\n", "\n", "\n", "String-1316606400713378063\n", "\n", "\n", ""X"\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-4801791173778264996\n", "\n", "\n", "Matrix_named\n", "\n", "\n", "\n", "\n", "\n", "\n", "String-4801791173778264996\n", "\n", "\n", ""Y"\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix___matmul__-5871781006564002453\n", "\n", "\n", "Matrix___matmul__\n", "\n", "\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "outer_cluster_String-4801791173778264996\n", "\n", "\n", "cluster_String-4801791173778264996\n", "\n", "\n", "\n", "outer_cluster_String-1316606400713378063\n", "\n", "\n", "cluster_String-1316606400713378063\n", "\n", "\n", "\n", "outer_cluster_2\n", "\n", "\n", "cluster_2\n", "\n", "\n", "\n", "outer_cluster_0\n", "\n", "\n", "cluster_0\n", "\n", "\n", "\n", "outer_cluster_1\n", "\n", "\n", "cluster_1\n", "\n", "\n", "\n", "outer_cluster_4\n", "\n", "\n", "cluster_4\n", "\n", "\n", "\n", "outer_cluster_3\n", "\n", "\n", "cluster_3\n", "\n", "\n", "\n", "outer_cluster_5\n", "\n", "\n", "cluster_5\n", "\n", "\n", "\n", "outer_cluster_6\n", "\n", "\n", "cluster_6\n", "\n", "\n", "\n", "\n", "Matrix___matmul__-5871781006564002453:s->Matrix_named-1316606400713378063\n", "\n", "\n", "\n", "\n", "\n", "Matrix___matmul__-5871781006564002453:s->Matrix_named-4801791173778264996\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-1316606400713378063:s->String-1316606400713378063\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-4801791173778264996:s->String-4801791173778264996\n", "\n", "\n", "\n", "\n", "\n", "Matrix_nrows-0:s->Matrix_named-1316606400713378063\n", "\n", "\n", "\n", "\n", "\n", "Matrix_ncols-0:s->Matrix_named-1316606400713378063\n", "\n", "\n", "\n", "\n", "\n", "Matrix_nrows-5871781006564002453:s->Matrix_named-4801791173778264996\n", "\n", "\n", "\n", "\n", "\n", "Matrix_ncols-5871781006564002453:s->Matrix_named-4801791173778264996\n", "\n", "\n", "\n", "\n", "\n", "Matrix___matmul__-5871781006564002453\n", "\n", "\n", "Matrix___matmul__\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-1316606400713378063\n", "\n", "\n", "Matrix_named\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_named-4801791173778264996\n", "\n", "\n", "Matrix_named\n", "\n", "\n", "\n", "\n", "\n", "\n", "String-1316606400713378063\n", "\n", "\n", ""X"\n", "\n", "\n", "\n", "\n", "\n", "\n", "String-4801791173778264996\n", "\n", "\n", ""Y"\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_nrows-0\n", "\n", "\n", "Matrix_nrows\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_ncols-0\n", "\n", "\n", "Matrix_ncols\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_nrows-5871781006564002453\n", "\n", "\n", "Matrix_nrows\n", "\n", "\n", "\n", "\n", "\n", "\n", "Matrix_ncols-5871781006564002453\n", "\n", "\n", "Matrix_ncols\n", "\n", "\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "with egraph:\n", " egraph.register(Matrix.named(\"X\") @ Matrix.named(\"Y\"))\n", " egraph.display()\n", " egraph.run(1)\n", " egraph.display()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "bd9e94de", "metadata": {}, "source": [ "We can try this out in our notebook, by multiplying some matrices and checking their dimensions:\n" ] }, { "cell_type": "code", "execution_count": 13, "id": "bb50ade6", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
kron(Matrix.named("A"), Matrix.named("B"))\n",
       "
\n" ], "text/latex": [ "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", "\\PY{n}{kron}\\PY{p}{(}\\PY{n}{Matrix}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{A}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)}\\PY{p}{,} \\PY{n}{Matrix}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{B}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)}\\PY{p}{)}\n", "\\end{Verbatim}\n" ], "text/plain": [ "kron(Matrix.named(\"A\"), Matrix.named(\"B\"))" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Define a number of dimensions\n", "n, m, p = Dim.named(\"n\"), Dim.named(\"m\"), Dim.named(\"p\")\n", "# Define a number of matrices\n", "A, B, C = Matrix.named(\"A\"), Matrix.named(\"B\"), Matrix.named(\"C\")\n", "# Set each to be a square matrix of the given dimension\n", "egraph.register(\n", " union(A.nrows()).with_(n),\n", " union(A.ncols()).with_(n),\n", " union(B.nrows()).with_(m),\n", " union(B.ncols()).with_(m),\n", " union(C.nrows()).with_(p),\n", " union(C.ncols()).with_(p),\n", ")\n", "# Create an example which should equal the kronecker product of A and B\n", "ex1 = kron(Matrix.identity(n), B) @ kron(A, Matrix.identity(m))\n", "egraph.simplify(ex1, 20)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "554321e2", "metadata": {}, "source": [ "We can make sure that if the rows/columns do not line up, then the transformation will not be applied:\n" ] }, { "cell_type": "code", "execution_count": 14, "id": "d8dea199", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
kron(Matrix.identity(Dim.named("p")), Matrix.named("C")) @ kron(Matrix.named("A"), Matrix.identity(Dim.named("m")))\n",
       "
\n" ], "text/latex": [ "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", "\\PY{n}{kron}\\PY{p}{(}\\PY{n}{Matrix}\\PY{o}{.}\\PY{n}{identity}\\PY{p}{(}\\PY{n}{Dim}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{p}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)}\\PY{p}{)}\\PY{p}{,} \\PY{n}{Matrix}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{C}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)}\\PY{p}{)} \\PY{o}{@} \\PY{n}{kron}\\PY{p}{(}\\PY{n}{Matrix}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{A}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)}\\PY{p}{,} \\PY{n}{Matrix}\\PY{o}{.}\\PY{n}{identity}\\PY{p}{(}\\PY{n}{Dim}\\PY{o}{.}\\PY{n}{named}\\PY{p}{(}\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{m}\\PY{l+s+s2}{\\PYZdq{}}\\PY{p}{)}\\PY{p}{)}\\PY{p}{)}\n", "\\end{Verbatim}\n" ], "text/plain": [ "kron(Matrix.identity(Dim.named(\"p\")), Matrix.named(\"C\")) @ kron(Matrix.named(\"A\"), Matrix.identity(Dim.named(\"m\")))" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "ex2 = kron(Matrix.identity(p), C) @ kron(A, Matrix.identity(m))\n", "egraph.simplify(ex2, 20)" ] }, { "cell_type": "code", "execution_count": null, "id": "b0b13665", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "file_format": "mystnb", "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.12" } }, "nbformat": 4, "nbformat_minor": 5 }