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:
- Experimenting with generics in Rust: little library for Bezier curves - part 1.
- Generics in Rust: little library for Bezier curves -- Part 2.
- and GitHub repository with source code and some examples.
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
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
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" }
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);
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
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).
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.
Top comments (0)