Generics in Rust: visualizing Bezier curves in a Jupyter notebook -- Part 3

iprosk

Igor Proskurin

Posted on May 31, 2024

Generics in Rust: visualizing Bezier curves in a Jupyter notebook -- Part 3

I decided to write a series posts about my experience with generic Rust, basically just to leave a trace of bread crumbs in my scattered studies. As a small practical problem, I chose to implement a library for manipulating generic Bezier curves that would work with different types and would wrap around primitive stack allocated arrays without dynamic binding and heap allocations. Here, there are some artefacts:

In this post, I would briefly outline steps to visualize Bezier curves from my library by using Rust from a Jupyter notebook with Plotters.

Jupyter notebooks really revolutionized not only data science but scientific computing in general. Conceived originally for REPL (read-eval-print-loop) languages such as Julia, Python, and R, Jupyter notebooks are available now even for C++11, and, of course, Rust is not an exception.

First comes REPL - evxvr

A project for Rust REPL environment is a combination of letter evxvr (Evaluation Context for Rust). It contains Evcxr Jupyter kernel. I chose to follow the documentation and compile Jupyter kernel from Rust sources (which takes about 6 min on my laptop), and simply run in Microsoft Windows PowerShell

cargo install --locked evcxr_jupyter
Enter fullscreen mode Exit fullscreen mode

This compiles the binary that can be found in $HOME\.cargo\bin. I already have Jupyter server installed as part of my Anaconda Python bundle. So after that, simply run

evcxr_jupyter --install
Enter fullscreen mode Exit fullscreen mode

And that's all. Now, when I start Jupyter Server, I can choose Rust kernel, and use it from an interactive environment.

My overall impression from Rust notebooks is that they feel less smooth than, for example, Python notebooks (not surprising), but its pretty usable. Some extra work should be done to implement custom output for user-defined types.

Adding external dependencies

It is easy to add external dependencies directly to the Jupyter notebook. I just add the following lines to a notebook cell

:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
:dep num = {version = "0.4.3"}
:dep bernstein = { git = "https://github.com/sciprosk/bernstein.git" }
Enter fullscreen mode Exit fullscreen mode

and then run it. It takes some visible time to run it for the first time, but after that it is fast. The last line adds my little library for Bezier curves directly from GitHub repo. Then I can put the following code into the next cell, and it works.

use bernstein::Bernstein;
use num::Complex;
use num::FromPrimitive;
use std::array;

// Create 2D Bezier control polygon in the complex plane
let p0 = Complex::new(0.0, 0.0);
let p1 = Complex::new(2.5, 1.0);
let p2 = Complex::new(-0.5, 1.0);
let p3 = Complex::new(2.0, 0.0);

// 2D Bezier curve in the complex plane parameterized with f32
let c: Bernstein<Complex<f32>, f32, 4> = Bernstein::new([p0, p1, p2, p3]);

// Just sample some points on the curve into array
let cs:[_; 11] = array::from_fn(
    |x| -> Complex<f32> {
        c.eval(f32::from_usize(x).unwrap() / 10.0)
    }
);

println!("{:?}", cs);
Enter fullscreen mode Exit fullscreen mode

Plotting with Plotters

One of the crates that integrates evcxr is Plotters, which is used for ... well, you already know it.

Plotters can use different backends, and one them is evcxr_figure that allows to draw directly to the Jupyter notebook cells. The syntax is mostly self-explanatory.

use plotters::prelude::*;

let figure = evcxr_figure((800, 640), |root| {
    root.fill(&WHITE)?;
    let mut chart = ChartBuilder::on(&root)
        .caption("Cubic Bezier", ("Arial", 30).into_font())
        .margin(5)
        .x_label_area_size(30)
        .y_label_area_size(30)
        .build_cartesian_2d(-0.2f32..2.1f32, -0.8f32..0.8f32)?;

    chart.configure_mesh().draw()?;

    // Cubic Bezier curve
    chart.draw_series(LineSeries::new(
        // Sample 20_000 points
        (0..=20000).map(|x| x as f32 / 20000.0).map(|x| (c.eval(x).re, c.eval(x).im)),
        &RED,
    )).unwrap()
        .label("Cubic Bezier")
        .legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &RED));

    // Derivative, scaled down
    chart.draw_series(LineSeries::new(
        // Sample 20_000 points
        (0..=20000).map(|x| x as f32 / 20000.0).map(|x| (0.2 * c.diff().eval(x).re, 0.2 * c.diff().eval(x).im)),
        &BLUE,
    )).unwrap()
        .label("Derivative")
        .legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &BLUE));

    chart.configure_series_labels()
        .background_style(&WHITE.mix(0.8))
        .border_style(&BLACK)
        .draw()?;
    Ok(())
});
figure
Enter fullscreen mode Exit fullscreen mode

This creates a 800x600 figure, fills it with white, sets the ranges to -0.2f32..2.1f32 along the horizontal axis, and to -0.8f32..0.8f32 along the vertical axis, and finally samples 20000 points of the cubic Bezier curve and its parametric derivative (which is a quadratic Bezier curve -- scaled down to fit).
Image description

Summary

Interactive Rust is easy to install and straightforward to use. It requires more typing (sometimes struggling) than when using REPL in duck typed languages, which is a price of strong (and strict) type system. However, it is a cool tool for data visualization directly from Rust.

💖 💪 🙅 🚩
iprosk
Igor Proskurin

Posted on May 31, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related