Last active
April 9, 2026 22:29
-
-
Save alexlib/ab0db711f9133b9709c573f1f96aca17 to your computer and use it in GitHub Desktop.
rustimport_jupyter_openpiv_rust.ipynb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "view-in-github", | |
| "colab_type": "text" | |
| }, | |
| "source": [ | |
| "<a href=\"https://colab.research.google.com/gist/alexlib/ab0db711f9133b9709c573f1f96aca17/rustimport_jupyter_openpiv_rust.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "R60MRpVGEykF" | |
| }, | |
| "source": [ | |
| "# Python Extensions in Rust with Jupyter Notebooks" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 15, | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "3XOBCO5XEykX", | |
| "outputId": "e7ab0d4b-8ec8-4d18-8c51-f0a683775fbb" | |
| }, | |
| "outputs": [ | |
| { | |
| "output_type": "stream", | |
| "name": "stdout", | |
| "text": [ | |
| "Requirement already satisfied: rustimport_jupyter in /usr/local/lib/python3.12/dist-packages (0.1.3)\n", | |
| "Requirement already satisfied: polars==0.20.2 in /usr/local/lib/python3.12/dist-packages (0.20.2)\n", | |
| "Requirement already satisfied: ipython in /usr/local/lib/python3.12/dist-packages (from rustimport_jupyter) (7.34.0)\n", | |
| "Requirement already satisfied: rustimport in /usr/local/lib/python3.12/dist-packages (from rustimport_jupyter) (1.7.1)\n", | |
| "Requirement already satisfied: setuptools>=18.5 in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (75.2.0)\n", | |
| "Requirement already satisfied: jedi>=0.16 in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (0.19.2)\n", | |
| "Requirement already satisfied: decorator in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (4.4.2)\n", | |
| "Requirement already satisfied: pickleshare in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (0.7.5)\n", | |
| "Requirement already satisfied: traitlets>=4.2 in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (5.7.1)\n", | |
| "Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (3.0.52)\n", | |
| "Requirement already satisfied: pygments in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (2.20.0)\n", | |
| "Requirement already satisfied: backcall in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (0.2.0)\n", | |
| "Requirement already satisfied: matplotlib-inline in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (0.2.1)\n", | |
| "Requirement already satisfied: pexpect>4.3 in /usr/local/lib/python3.12/dist-packages (from ipython->rustimport_jupyter) (4.9.0)\n", | |
| "Requirement already satisfied: toml>=0.10.2 in /usr/local/lib/python3.12/dist-packages (from rustimport->rustimport_jupyter) (0.10.2)\n", | |
| "Requirement already satisfied: parso<0.9.0,>=0.8.4 in /usr/local/lib/python3.12/dist-packages (from jedi>=0.16->ipython->rustimport_jupyter) (0.8.6)\n", | |
| "Requirement already satisfied: ptyprocess>=0.5 in /usr/local/lib/python3.12/dist-packages (from pexpect>4.3->ipython->rustimport_jupyter) (0.7.0)\n", | |
| "Requirement already satisfied: wcwidth in /usr/local/lib/python3.12/dist-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython->rustimport_jupyter) (0.6.0)\n", | |
| "\u001b[1minfo:\u001b[0m downloading installer\n", | |
| "\u001b[1m\u001b[33mwarn:\u001b[0m It looks like you have an existing rustup settings file at:\n", | |
| "\u001b[1m\u001b[33mwarn:\u001b[0m /root/.rustup/settings.toml\n", | |
| "\u001b[1m\u001b[33mwarn:\u001b[0m Rustup will install the default toolchain as specified in the settings file,\n", | |
| "\u001b[1m\u001b[33mwarn:\u001b[0m instead of the one inferred from the default host triple.\n", | |
| "\u001b[1minfo:\u001b[0m profile set to default\n", | |
| "\u001b[1minfo:\u001b[0m default host triple is x86_64-unknown-linux-gnu\n", | |
| "\u001b[1m\u001b[33mwarn:\u001b[0m Updating existing toolchain, profile choice will be ignored\n", | |
| "\u001b[1minfo:\u001b[0m syncing channel updates for stable-x86_64-unknown-linux-gnu\n", | |
| "\u001b[1minfo:\u001b[0m default toolchain set to stable-x86_64-unknown-linux-gnu\n", | |
| "\n", | |
| " \u001b[1mstable-x86_64-unknown-linux-gnu unchanged\u001b[0m - rustc 1.94.1 (e408947bf 2026-03-25)\n", | |
| "\n", | |
| "\u001b[0m\u001b[1m\n", | |
| "Rust is installed now. Great!\n", | |
| "\u001b[0m\n", | |
| "To get started you may need to restart your current shell.\n", | |
| "This would reload your \u001b[0m\u001b[1mPATH\u001b[0m environment variable to include\n", | |
| "Cargo's bin directory ($HOME/.cargo/bin).\n", | |
| "\n", | |
| "To configure your current shell, you need to source\n", | |
| "the corresponding \u001b[0m\u001b[1menv\u001b[0m file under $HOME/.cargo.\n", | |
| "\n", | |
| "This is usually done by running one of the following (note the leading DOT):\n", | |
| ". \"$HOME/.cargo/env\" # For sh/bash/zsh/ash/dash/pdksh\n", | |
| "source \"$HOME/.cargo/env.fish\" # For fish\n", | |
| "source \"~/.cargo/env.nu\" # For nushell\n", | |
| "source \"$HOME/.cargo/env.tcsh\" # For tcsh\n", | |
| ". \"$HOME/.cargo/env.ps1\" # For pwsh\n", | |
| "source \"$HOME/.cargo/env.xsh\" # For xonsh\n", | |
| "The rustimport_jupyter extension is already loaded. To reload it, use:\n", | |
| " %reload_ext rustimport_jupyter\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "# Install dependencies for google colab\n", | |
| "import os\n", | |
| "import sys\n", | |
| "\n", | |
| "IN_COLAB = \"google.colab\" in sys.modules\n", | |
| "if IN_COLAB:\n", | |
| " !pip install -U rustimport_jupyter polars==0.20.2\n", | |
| " !curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n", | |
| " os.environ[\"PATH\"] += \":/root/.cargo/bin\"\n", | |
| " import rustimport_jupyter\n", | |
| " %load_ext rustimport_jupyter" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "BTfxKu3LEykm" | |
| }, | |
| "source": [ | |
| "The Rust programming language has gotten more prominent for writing compiled Python extensions. Currently, there is a bunch of boilerplate for wrapping writing up a Rust function and making it callable from Python. I enjoy exploring and prototyping code in Jupyter Notebooks, so I developed [rustimport_jupyter](https://github.com/thomasjpfan/rustimport_jupyter) to compile Rust code in Jupyter and have the compiled code available in Python! In this blog post, I will showcase a simple function, NumPy function, and Polar expression plugins. This blog post is runnable as a [notebook on Google Colab](https://colab.research.google.com/github/thomasjpfan/thomasjpfan.github.io/blob/main/content/notebooks/20231227_rustimport_jupyter.ipynb)." | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "34mXlPJpEykp" | |
| }, | |
| "source": [ | |
| "## Simple Rust Functions\n", | |
| "\n", | |
| "`rustimport_jupyter` builds on top of [rustimport](https://github.com/mityax/rustimport) to compile Python extensions written in Rust from Jupyter notebooks. After installing the [rustimport_jupyter](https://pypi.org/project/rustimport-jupyter/) package from PyPI, we load the magic from within a Jupyter notebook:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 16, | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "T7lMEuBhEyky", | |
| "outputId": "413b3d95-e690-4615-a6b9-788fc64874e7" | |
| }, | |
| "outputs": [ | |
| { | |
| "output_type": "stream", | |
| "name": "stdout", | |
| "text": [ | |
| "The rustimport_jupyter extension is already loaded. To reload it, use:\n", | |
| " %reload_ext rustimport_jupyter\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "%load_ext rustimport_jupyter" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "vLACR89BEykz" | |
| }, | |
| "source": [ | |
| "Next, we define a `double` function in Rust and prefixing the cell with the `%%rustimport` marker:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 17, | |
| "metadata": { | |
| "id": "5Ah20mv9Eyk0" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "%%rustimport\n", | |
| "use pyo3::prelude::*;\n", | |
| "\n", | |
| "#[pyfunction]\n", | |
| "fn double(x: i32) -> i32 {\n", | |
| " 2 * x\n", | |
| "}" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "ajvatJ8qEyk6" | |
| }, | |
| "source": [ | |
| "The `%%rustimport` marker compiles the Rust code and imports the `double` function into the Jupyter notebook environment. This means, we can directly call it from Python!" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 18, | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "rIbHJq9GEylE", | |
| "outputId": "0da2a973-e1aa-494c-d3a7-5a83ec6d377d" | |
| }, | |
| "outputs": [ | |
| { | |
| "output_type": "execute_result", | |
| "data": { | |
| "text/plain": [ | |
| "68" | |
| ] | |
| }, | |
| "metadata": {}, | |
| "execution_count": 18 | |
| } | |
| ], | |
| "source": [ | |
| "double(34)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "YR7FqKYCEylN" | |
| }, | |
| "source": [ | |
| "By default, `%%rustimport` is compiles without Rust optimizations. We can enable these optimizations by adding the `--release` flag:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 19, | |
| "metadata": { | |
| "id": "OiWw8AYdEylP" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "%%rustimport --release\n", | |
| "use pyo3::prelude::*;\n", | |
| "\n", | |
| "#[pyfunction]\n", | |
| "fn triple(x: i32) -> i32 {\n", | |
| " 3 * x\n", | |
| "}" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 20, | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "jRrbItq3EylU", | |
| "outputId": "c45095fe-6177-4076-cd5a-5bc59f761ff8" | |
| }, | |
| "outputs": [ | |
| { | |
| "output_type": "execute_result", | |
| "data": { | |
| "text/plain": [ | |
| "21" | |
| ] | |
| }, | |
| "metadata": {}, | |
| "execution_count": 20 | |
| } | |
| ], | |
| "source": [ | |
| "triple(7)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "orSltr0bEylV" | |
| }, | |
| "source": [ | |
| "## NumPy in Rust\n", | |
| "\n", | |
| "Rust's ecosystem contains many [third party libraries](https://crates.io) that is useful for writing our custom functions. [rustimport](https://github.com/mityax/rustimport#customizing-an-extension) defines a custom `//:` comment syntax that we can use to pull in write our extensions. In this next example, we use [PyO3/rust-numpy](https://github.com/PyO3/rust-numpy) to define a NumPy function that computes `a*x-y` in Rust:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 21, | |
| "metadata": { | |
| "id": "Q63ArSViEylZ" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "%%rustimport --release\n", | |
| "//: [dependencies]\n", | |
| "//: pyo3 = { version = \"0.21\", features = [\"extension-module\"] }\n", | |
| "//: numpy = \"0.21\"\n", | |
| "\n", | |
| "use pyo3::prelude::*;\n", | |
| "use numpy::ndarray::{ArrayD, ArrayViewD};\n", | |
| "use numpy::{IntoPyArray, PyArrayDyn, PyReadonlyArrayDyn};\n", | |
| "\n", | |
| "fn axsy(a: f64, x: ArrayViewD<'_, f64>, y: ArrayViewD<'_, f64>) -> ArrayD<f64> {\n", | |
| " a * &x - &y\n", | |
| "}\n", | |
| "\n", | |
| "#[pyfunction]\n", | |
| "#[pyo3(name = \"axsy\")]\n", | |
| "fn axsy_py<'py>(\n", | |
| " py: Python<'py>,\n", | |
| " a: f64,\n", | |
| " x: PyReadonlyArrayDyn<'py, f64>,\n", | |
| " y: PyReadonlyArrayDyn<'py, f64>,\n", | |
| ") -> Bound<'py, PyArrayDyn<f64>> {\n", | |
| " let x = x.as_array();\n", | |
| " let y = y.as_array();\n", | |
| " let z = axsy(a, x, y);\n", | |
| " z.into_pyarray_bound(py)\n", | |
| "}\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "IGdRGEdPEylg" | |
| }, | |
| "source": [ | |
| "The `pyo3(name = \"axsy\")` Rust macro exports the compiled function as `axsy` in Python. We can now use `axsy` directly in Jupyter:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 22, | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "EuRMn4MMEylh", | |
| "outputId": "e735ebf7-dfb7-4acd-d7a9-f5ba659f86e8" | |
| }, | |
| "outputs": [ | |
| { | |
| "output_type": "execute_result", | |
| "data": { | |
| "text/plain": [ | |
| "array([ 0.3, -8.2, 5.6])" | |
| ] | |
| }, | |
| "metadata": {}, | |
| "execution_count": 22 | |
| } | |
| ], | |
| "source": [ | |
| "import numpy as np\n", | |
| "\n", | |
| "a = 2.4\n", | |
| "x = np.array([1.0, -3.0, 4.0], dtype=np.float64)\n", | |
| "y = np.array([2.1, 1.0, 4.0], dtype=np.float64)\n", | |
| "\n", | |
| "axsy(a, x, y)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": [ | |
| "%%rustimport --release\n", | |
| "//: [dependencies]\n", | |
| "//: pyo3 = { version = \"0.21\", features = [\"extension-module\"] }\n", | |
| "//: numpy = \"0.21\"\n", | |
| "//: ndarray = \"0.15\"\n", | |
| "\n", | |
| "use pyo3::prelude::*;\n", | |
| "use numpy::{PyArray2, PyArrayMethods, ToPyArray, PyUntypedArrayMethods};\n", | |
| "use ndarray::{Array2, s};\n", | |
| "\n", | |
| "#[pyfunction]\n", | |
| "pub fn run_piv(py: Python, img1: &Bound<'_, PyArray2<u8>>, img2: &Bound<'_, PyArray2<u8>>, win_size: usize, search_size: usize) -> PyResult<Py<PyArray2<isize>>> {\n", | |
| " let view1 = img1.readonly();\n", | |
| " let view2 = img2.readonly();\n", | |
| " let view1 = view1.as_array();\n", | |
| " let view2 = view2.as_array();\n", | |
| "\n", | |
| " let (h, w) = (view1.nrows(), view1.ncols());\n", | |
| " let mut vectors = Array2::zeros((h / win_size, w / win_size));\n", | |
| "\n", | |
| " for i in (0..h - win_size).step_by(win_size) {\n", | |
| " for j in (0..w - win_size).step_by(win_size) {\n", | |
| " let window1 = view1.slice(s![i..i+win_size, j..j+win_size]);\n", | |
| " let mut max_corr = -1.0;\n", | |
| " let mut best_shift = 0isize;\n", | |
| "\n", | |
| " let half_search = (search_size / 2) as isize;\n", | |
| " for dy in -half_search..half_search {\n", | |
| " for dx in -half_search..half_search {\n", | |
| " let y_start = i as isize + dy;\n", | |
| " let x_start = j as isize + dx;\n", | |
| " if y_start < 0 || x_start < 0 || (y_start as usize + win_size) > h || (x_start as usize + win_size) > w {\n", | |
| " continue;\n", | |
| " }\n", | |
| " let window2 = view2.slice(s![y_start as usize..y_start as usize + win_size, x_start as usize..x_start as usize + win_size]);\n", | |
| " let mut corr = 0.0;\n", | |
| " for r in 0..win_size {\n", | |
| " for c in 0..win_size {\n", | |
| " corr += (window1[[r, c]] as f32) * (window2[[r, c]] as f32);\n", | |
| " }\n", | |
| " }\n", | |
| " if corr > max_corr {\n", | |
| " max_corr = corr;\n", | |
| " best_shift = dx;\n", | |
| " }\n", | |
| " }\n", | |
| " }\n", | |
| " vectors[[i / win_size, j / win_size]] = best_shift;\n", | |
| " }\n", | |
| " }\n", | |
| " Ok(vectors.to_pyarray_bound(py).unbind())\n", | |
| "}" | |
| ], | |
| "metadata": { | |
| "id": "-hO1y0JtJpg9" | |
| }, | |
| "execution_count": 23, | |
| "outputs": [] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "e6e1f623", | |
| "outputId": "d492e6a7-fb0b-4d7b-d5a0-a78eaf6b5f2d" | |
| }, | |
| "source": [ | |
| "import numpy as np\n", | |
| "import time\n", | |
| "\n", | |
| "def generate_synthetic_data(size=512, shift=3):\n", | |
| " img1 = np.random.randint(0, 256, (size, size), dtype=np.uint8)\n", | |
| " img2 = np.roll(img1, shift, axis=0)\n", | |
| " img2 = np.roll(img2, shift, axis=1)\n", | |
| " return img1, img2\n", | |
| "\n", | |
| "def basic_piv_python(img1, img2, win_size=32, search_size=10):\n", | |
| " h, w = img1.shape\n", | |
| " vectors = np.zeros((h // win_size, w // win_size), dtype=np.int64)\n", | |
| " for i in range(0, h - win_size, win_size):\n", | |
| " for j in range(0, w - win_size, win_size):\n", | |
| " window1 = img1[i:i+win_size, j:j+win_size].astype(np.float32)\n", | |
| " max_corr = -1\n", | |
| " best_shift = 0\n", | |
| " for dy in range(-search_size // 2, search_size // 2):\n", | |
| " for dx in range(-search_size // 2, search_size // 2):\n", | |
| " y_start, x_start = i + dy, j + dx\n", | |
| " if y_start < 0 or x_start < 0 or y_start + win_size > h or x_start + win_size > w:\n", | |
| " continue\n", | |
| " window2 = img2[y_start:y_start+win_size, x_start:x_start+win_size].astype(np.float32)\n", | |
| " corr = np.sum(window1 * window2)\n", | |
| " if corr > max_corr:\n", | |
| " max_corr = corr\n", | |
| " best_shift = dx\n", | |
| " vectors[i//win_size, j//win_size] = best_shift\n", | |
| " return vectors\n", | |
| "\n", | |
| "# Initialize data\n", | |
| "img1, img2 = generate_synthetic_data(size=256)\n", | |
| "win, search = 16, 10\n", | |
| "\n", | |
| "print(\"--- Benchmark Results ---\")\n", | |
| "# Test Pure Python\n", | |
| "start = time.time()\n", | |
| "res_py = basic_piv_python(img1, img2, win, search)\n", | |
| "py_time = time.time() - start\n", | |
| "print(f\"Pure Python Time: {py_time:.4f}s\")\n", | |
| "\n", | |
| "# Test Rust-backed Python\n", | |
| "start = time.time()\n", | |
| "# run_piv was imported by the previous %%rustimport cell\n", | |
| "res_rs = run_piv(img1, img2, win, search)\n", | |
| "rs_time = time.time() - start\n", | |
| "print(f\"Rust-backed Python Time: {rs_time:.4f}s\")\n", | |
| "\n", | |
| "print(f\"Speedup: {py_time / rs_time:.1f}x\")" | |
| ], | |
| "execution_count": 24, | |
| "outputs": [ | |
| { | |
| "output_type": "stream", | |
| "name": "stdout", | |
| "text": [ | |
| "--- Benchmark Results ---\n", | |
| "Pure Python Time: 0.1507s\n", | |
| "Rust-backed Python Time: 0.0097s\n", | |
| "Speedup: 15.5x\n" | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "4867cfe6", | |
| "outputId": "4309cd02-50fc-4c11-8926-f90c5dd682d1" | |
| }, | |
| "source": [ | |
| "def vectorized_piv_numpy(img1, img2, win_size=32, search_size=10):\n", | |
| " # Pre-convert to float32 once to avoid overhead in the loop\n", | |
| " img1_f = img1.astype(np.float32)\n", | |
| " img2_f = img2.astype(np.float32)\n", | |
| "\n", | |
| " h, w = img1.shape\n", | |
| " vectors = np.zeros((h // win_size, w // win_size), dtype=np.int64)\n", | |
| " half_search = search_size // 2\n", | |
| "\n", | |
| " for i in range(0, h - win_size, win_size):\n", | |
| " for j in range(0, w - win_size, win_size):\n", | |
| " window1 = img1_f[i:i+win_size, j:j+win_size]\n", | |
| " max_corr = -1\n", | |
| " best_shift = 0\n", | |
| "\n", | |
| " for dy in range(-half_search, half_search):\n", | |
| " for dx in range(-half_search, half_search):\n", | |
| " y_s, x_s = i + dy, j + dx\n", | |
| " if y_s < 0 or x_s < 0 or y_s + win_size > h or x_s + win_size > w:\n", | |
| " continue\n", | |
| "\n", | |
| " window2 = img2_f[y_s:y_s+win_size, x_s:x_s+win_size]\n", | |
| " # Optimized NumPy operation\n", | |
| " corr = np.vdot(window1, window2)\n", | |
| "\n", | |
| " if corr > max_corr:\n", | |
| " max_corr = corr\n", | |
| " best_shift = dx\n", | |
| " vectors[i//win_size, j//win_size] = best_shift\n", | |
| " return vectors\n", | |
| "\n", | |
| "print(\"--- Optimized Three-Way Benchmark ---\")\n", | |
| "# 1. Pure Python\n", | |
| "start = time.time()\n", | |
| "res_py = basic_piv_python(img1, img2, win, search)\n", | |
| "py_time = time.time() - start\n", | |
| "print(f\"Basic Python Time: {py_time:.4f}s\")\n", | |
| "\n", | |
| "# 2. NumPy Vectorized (Optimized)\n", | |
| "start = time.time()\n", | |
| "res_np = vectorized_piv_numpy(img1, img2, win, search)\n", | |
| "np_time = time.time() - start\n", | |
| "print(f\"NumPy Optimized Time: {np_time:.4f}s\")\n", | |
| "\n", | |
| "# 3. Rust\n", | |
| "start = time.time()\n", | |
| "res_rs = run_piv(img1, img2, win, search)\n", | |
| "rs_time = time.time() - start\n", | |
| "print(f\"Rust-backed Time: {rs_time:.4f}s\")\n", | |
| "\n", | |
| "print(\"\\nNew Speedup relative to Basic Python:\")\n", | |
| "print(f\"NumPy (Optimized): {py_time / np_time:.1f}x\")\n", | |
| "print(f\"Rust: {py_time / rs_time:.1f}x\")\n", | |
| "\n", | |
| "# Consistency check between the two FFT implementations\n", | |
| "matches = np.sum(res_py == res_rs)\n", | |
| "print(f\"Consistency between Python and Rust versions: {matches}/{res_py.size} matches\")\n", | |
| "\n", | |
| "# Consistency check between the two FFT implementations\n", | |
| "matches = np.sum(res_np == res_rs)\n", | |
| "print(f\"Consistency between Numpy and Rust versions: {matches}/{res_py.size} matches\")" | |
| ], | |
| "execution_count": 25, | |
| "outputs": [ | |
| { | |
| "output_type": "stream", | |
| "name": "stdout", | |
| "text": [ | |
| "--- Optimized Three-Way Benchmark ---\n", | |
| "Basic Python Time: 0.1500s\n", | |
| "NumPy Optimized Time: 0.0572s\n", | |
| "Rust-backed Time: 0.0099s\n", | |
| "\n", | |
| "New Speedup relative to Basic Python:\n", | |
| "NumPy (Optimized): 2.6x\n", | |
| "Rust: 15.2x\n", | |
| "Consistency between Python and Rust versions: 256/256 matches\n", | |
| "Consistency between Numpy and Rust versions: 256/256 matches\n" | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": [ | |
| "def fft_piv_numpy(img1, img2, win_size):\n", | |
| " \"\"\"Perform PIV using FFT-based cross-correlation in NumPy.\"\"\"\n", | |
| " img1 = img1.astype(np.float32)\n", | |
| " img2 = img2.astype(np.float32)\n", | |
| " h, w = img1.shape\n", | |
| " vectors = np.zeros((h // win_size, w // win_size), dtype=np.int64)\n", | |
| "\n", | |
| " for i in range(0, h - win_size, win_size):\n", | |
| " for j in range(0, w - win_size, win_size):\n", | |
| " sub1 = img1[i:i+win_size, j:j+win_size]\n", | |
| " sub2 = img2[i:i+win_size, j:j+win_size]\n", | |
| "\n", | |
| " # Standard FFT-based cross-correlation\n", | |
| " f1 = np.fft.fft2(sub1)\n", | |
| " f2 = np.fft.fft2(np.flip(sub2))\n", | |
| " corr = np.real(np.fft.ifft2(f1 * f2))\n", | |
| "\n", | |
| " # Find the displacement\n", | |
| " max_idx = np.unravel_index(np.argmax(corr), corr.shape)\n", | |
| " vectors[i//win_size, j//win_size] = max_idx[1]\n", | |
| " return vectors" | |
| ], | |
| "metadata": { | |
| "id": "N21BNq_0Vm4t" | |
| }, | |
| "execution_count": 26, | |
| "outputs": [] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "id": "xC9A6SRhL5jM" | |
| }, | |
| "source": [ | |
| "%%rustimport --release\n", | |
| "//: [dependencies]\n", | |
| "//: pyo3 = { version = \"0.21\", features = [\"extension-module\"] }\n", | |
| "//: numpy = \"0.21\"\n", | |
| "//: ndarray = \"0.15\"\n", | |
| "//: rustfft = \"6.1\"\n", | |
| "\n", | |
| "use pyo3::prelude::*;\n", | |
| "use numpy::{PyArray2, PyArrayMethods, ToPyArray};\n", | |
| "use ndarray::{Array2, s};\n", | |
| "use rustfft::{FftPlanner, num_complex::Complex};\n", | |
| "\n", | |
| "#[pyfunction]\n", | |
| "pub fn run_piv_fft_rust(py: Python, img1: &Bound<'_, PyArray2<u8>>, img2: &Bound<'_, PyArray2<u8>>, win_size: usize) -> PyResult<Py<PyArray2<isize>>> {\n", | |
| " let view1 = img1.readonly();\n", | |
| " let view2 = img2.readonly();\n", | |
| " let view1 = view1.as_array();\n", | |
| " let view2 = view2.as_array();\n", | |
| "\n", | |
| " let (h, w) = (view1.nrows(), view1.ncols());\n", | |
| " let mut vectors = Array2::zeros((h / win_size, w / win_size));\n", | |
| "\n", | |
| " let mut planner = FftPlanner::new();\n", | |
| " let fft = planner.plan_fft_forward(win_size * win_size);\n", | |
| " let ifft = planner.plan_fft_inverse(win_size * win_size);\n", | |
| "\n", | |
| " for i in (0..h - win_size).step_by(win_size) {\n", | |
| " for j in (0..w - win_size).step_by(win_size) {\n", | |
| " let sub1 = view1.slice(s![i..i+win_size, j..j+win_size]);\n", | |
| " let sub2 = view2.slice(s![i..i+win_size, j..j+win_size]);\n", | |
| "\n", | |
| " let mut buf1: Vec<Complex<f32>> = sub1.iter().map(|&x| Complex::new(x as f32, 0.0)).collect();\n", | |
| " let mut buf2: Vec<Complex<f32>> = sub2.iter().map(|&x| Complex::new(x as f32, 0.0)).collect();\n", | |
| " buf2.reverse();\n", | |
| "\n", | |
| " fft.process(&mut buf1);\n", | |
| " fft.process(&mut buf2);\n", | |
| "\n", | |
| " for k in 0..buf1.len() {\n", | |
| " buf1[k] *= buf2[k];\n", | |
| " }\n", | |
| "\n", | |
| " ifft.process(&mut buf1);\n", | |
| "\n", | |
| " let mut max_val = -1.0;\n", | |
| " let mut max_pos = 0;\n", | |
| " for (idx, val) in buf1.iter().enumerate() {\n", | |
| " if val.re > max_val {\n", | |
| " max_val = val.re;\n", | |
| " max_pos = idx % win_size;\n", | |
| " }\n", | |
| " }\n", | |
| " vectors[[i / win_size, j / win_size]] = max_pos as isize;\n", | |
| " }\n", | |
| " }\n", | |
| " Ok(vectors.to_pyarray_bound(py).unbind())\n", | |
| "}" | |
| ], | |
| "execution_count": 27, | |
| "outputs": [] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "colab": { | |
| "base_uri": "https://localhost:8080/" | |
| }, | |
| "id": "xC9A6SRhL5jL", | |
| "outputId": "ab906cc1-aabd-43cb-a9d0-1f5bc392b911" | |
| }, | |
| "source": [ | |
| "import time\n", | |
| "import numpy as np\n", | |
| "\n", | |
| "print(\"--- FFT Performance: Rust vs Python ---\")\n", | |
| "\n", | |
| "# Re-initialize data for consistency\n", | |
| "img1_bench, img2_bench = generate_synthetic_data(size=256)\n", | |
| "win_bench = 16\n", | |
| "\n", | |
| "start = time.time()\n", | |
| "res_fft_py = fft_piv_numpy(img1_bench, img2_bench, win_bench)\n", | |
| "py_fft_time = time.time() - start\n", | |
| "print(f\"FFT (Python/NumPy) Time: {py_fft_time:.4f}s\")\n", | |
| "\n", | |
| "start = time.time()\n", | |
| "# This will now be defined once cell xC9A6SRhL5jM compiles\n", | |
| "res_fft_rs = run_piv_fft_rust(img1_bench, img2_bench, win_bench)\n", | |
| "rs_fft_time = time.time() - start\n", | |
| "print(f\"FFT (Rust) Time: {rs_fft_time:.4f}s\")\n", | |
| "\n", | |
| "print(f\"\\nRust FFT Speedup: {py_fft_time / rs_fft_time:.1f}x\")\n", | |
| "\n", | |
| "matches = np.sum(res_fft_py == res_fft_rs)\n", | |
| "print(f\"Consistency between FFT versions: {matches}/{res_fft_py.size} matches\")" | |
| ], | |
| "execution_count": 28, | |
| "outputs": [ | |
| { | |
| "output_type": "stream", | |
| "name": "stdout", | |
| "text": [ | |
| "--- FFT Performance: Rust vs Python ---\n", | |
| "FFT (Python/NumPy) Time: 0.0541s\n", | |
| "FFT (Rust) Time: 0.0010s\n", | |
| "\n", | |
| "Rust FFT Speedup: 53.6x\n", | |
| "Consistency between FFT versions: 256/256 matches\n" | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": [], | |
| "metadata": { | |
| "id": "KxaEI2_AXQ9_" | |
| }, | |
| "execution_count": null, | |
| "outputs": [] | |
| } | |
| ], | |
| "metadata": { | |
| "kernelspec": { | |
| "display_name": "rustimport-jupyter", | |
| "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.11.7" | |
| }, | |
| "colab": { | |
| "provenance": [], | |
| "include_colab_link": true | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 0 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment