Ruby / Rust linkage (5) Numerical calculation with Rutie ② Bezier

Series of articles table of contents

Introduction

Last time, I used Rutie, which connects Ruby and Rust, to call a simple numerical calculation function of Rust from Ruby. In terms of speed, it was much slower than letting Ruby do the same calculation. This is probably because the cost of calling Rust functions from Ruby was reasonable. Compared to the cost, the numerical calculation was too light.

Again, using Rutie, let's call Rust from Ruby to do numerical calculations. What is different from the last time

That place. The latter is especially important, and if you can do this, you'll be able to do a lot more in collaboration with Rust.

Subject

I want to calculate the Bezier curve. Reference: [Bezier curve-Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%99%E3%82%B8%E3%82%A7%E6%9B%B2%E7%B7% 9A)

For a cubic Bezier, if you give four points on the plane $ \ boldsymbol {p} _0 $, $ \ boldsymbol {p} _1 $, $ \ boldsymbol {p} _2 $, $ \ boldsymbol {p} _3 $ The curve is decided. This curve is displayed as a parameter as follows.

\boldsymbol{p}(t) = (1-t)^3 \boldsymbol{p}_0 + 3t(1-t)^2 \boldsymbol{p}_1 + 3t^2(1-t) \boldsymbol{p}_2 + t^3 \boldsymbol{p}_3 \quad\quad (0 \leqq t \leqq 1)

As you can see immediately, when $ t = 0 $, it is $ \ boldsymbol p_0 $, and when $ t = 1 $, it is $ \ boldsymbol p_3 $. In other words, it is a curve that starts at $ \ boldsymbol p_0 $ and ends at $ \ boldsymbol p_3 $. $ \ Boldsymbol {p} _1 $ and $ \ boldsymbol {p} _2 $ do not generally pass (may pass depending on the conditions).

$ \ Boldsymbol p_1 $ and $ \ boldsymbol p_2 $ are also called control points, $ \ boldsymbol p_1-\ boldsymbol p_0 $ is a tangent vector at $ t = 0 $, and $ \ boldsymbol p_3-\ boldsymbol p_2 $ is It is a tangent vector at $ t = 1 $.

Now, what we want to do is the position $ \ boldsymbol p (t) $ at any $ t $ given $ \ boldsymbol p_0 $, $ \ boldsymbol p_1 $, $ \ boldsymbol p_2 $, $ \ boldsymbol p_3 $. To get.

Since the $ x $ and $ y $ coordinates are not related to each other, for $ a_0 $, $ a_1 $, $ a_2 $, $ a_3 $,

B(t) = (1-t)^3 a_0 + 3t(1-t)^2 a_1 + 3t^2(1-t) a_2 + t^3 a_3

You can think of a function of the form. Prepare this for $ x $ coordinates and $ y $ coordinates respectively.

This function is called the third-order Bernstein polynomial [^ bern]. In other words, the theme this time is "Calculate the value of the third-order Bernstein polynomial."

[^ bern]: The name of this polynomial is sometimes written as English-style "Bernstein polynomial" or German-style "Bernstein ** ta ** in polynomial", but it is named after the former Soviet Union mathematician Бернштейн. Therefore, it was set as "Bernstein polynomial" in Russian style. Wikipedia's [Sergei Bernstein](https://ja.wikipedia.org/wiki/%E3%82%BB%E3%83%AB%E3%82%B2%E3%82%A4%E3%83 % BB% E3% 83% 99% E3% 83% AB% E3% 83% B3% E3% 82% B7% E3% 83% A5% E3% 83% 86% E3% 82% A4% E3% 83% B3 ), The birthplace is Odessa, a city facing the Black Sea. It is now the Republic of Ukraine, but it seems that it was the Russian Empire at that time. This surname is Yiddish (Jewish German) and means amber ko </ rt> amber haku </ rt> </ ruby>.

policy

For various $ t , we will calculate with the same coefficient ( a_0 $, $ a_1 $, $ a_2 $, $ a_3 $), so let's create a class. Give a coefficient to create an instance, and then give $ t $ to calculate the value of the polynomial.

Let's name the class CubicBezier. No, what I'm doing is calculating the Bernstein polynomial, so CubicBernstein may be more suitable for the content, but "Bezier" is better.

When implemented in Ruby,

class CubicBezier
  def initialize(a0, a1, a2, a3)
    @a0, @a1, @a2, @a3 = a0, a1, a2, a3
  end
  
  def value_at(t)
    s = 1 - t
    @a0 * s * s * s + 3 * @a1 * t * s * s + 3 * @a2 * t * t * s + @a3 * t * t * t
  end

  alias [] value_at
end

It feels like

The reason why the alias [] is applied to value_at is that it seems more like Ruby to calculate with [].

Anyway, I'm going to implement a class with the same function in Rutie.

Implementation: Rust side

How to implement a class in Rutie that has instance variables. Fortunately, Rutie's code has explanations and examples, so I tried and errored while looking at them, and I found something that worked. I don't understand the theory.

Until editing Cargo.toml

Same as before

cargo new cubic_bezier --lib

I will do it. And to Cargo.toml

Cargo.toml


[dependencies]
lazy_static = "1.4.0"
rutie = "0.8.1"

[lib]
crate-type = ["cdylib"]

Put in. This time we need a lazy_static crate.

(Addition 2020-10-01) The version of Rutie was set to " 0.7.0 ", but it has been changed to the latest version " 0.8.1 ". This eliminates the warning about the name CubicBezier when compiling with Rust 1.46. If anyone says, "I was able to compile with 0.7.0, but I couldn't compile with 0.8.1", please let me know.

Body

policy

I'm not sure at all, but when defining a Ruby class that uses instance variables with Rutie, it seems that the method is to prepare a Rust structure (struct) and wrap it (wrap here is). I'm not sure what it means). In this case, I want to create a Ruby class called CubicBezier that has instance variables ʻa0, ʻa1, ʻa2, and ʻa3, so I first define a structure that has such fields. If the name of the structure is CubicBezier, it will be worn, so I have no choice but to use RustCubicBezier. Define CubicBezier to wrap it.

code

Here's the whole code.

src/lib.rs


#[macro_use]
extern crate lazy_static;

#[macro_use]
extern crate rutie;

use rutie::{Object, Class, Float};

pub struct RustCubicBezier {
    a0: f64,
    a1: f64,
    a2: f64,
    a3: f64,
}

impl RustCubicBezier {
    pub fn value_at(&self, t: f64) -> f64 {
        let s = 1.0 - t;
        self.a0 * s * s * s + 3.0 * self.a1 * t * s * s + 3.0 * self.a2 * t * t * s + self.a3 * t * t * t
    }
}

wrappable_struct!(RustCubicBezier, CubicBezierWrapper, CUBIC_BEZIER_WRAPPER);

class!(CubicBezier);

methods!(
    CubicBezier,
    rtself,

    fn cubic_bezier_new(a0: Float, a1: Float, a2: Float, a3: Float) -> CubicBezier {
        let a0 = a0.unwrap().to_f64();
        let a1 = a1.unwrap().to_f64();
        let a2 = a2.unwrap().to_f64();
        let a3 = a3.unwrap().to_f64();

        let rcb = RustCubicBezier{a0: a0, a1: a1, a2: a2, a3: a3};

        Class::from_existing("CubicBezier").wrap_data(rcb, &*CUBIC_BEZIER_WRAPPER)
    }

    fn value_at(t: Float) -> Float {
        let t = t.unwrap().to_f64();
        Float::new(rtself.get_data(&*CUBIC_BEZIER_WRAPPER).value_at(t))
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_cubic_bezier() {
    Class::new("CubicBezier", None).define(|klass| {
        klass.def_self("new", cubic_bezier_new);
        klass.def("value_at", value_at);
        klass.def("[]", value_at);
    });
}

Explanations will be added to each part in the following sections.

RustCubicBezier

Definition of structs and their methods:

pub struct RustCubicBezier {
    a0: f64,
    a1: f64,
    a2: f64,
    a3: f64,
}

impl RustCubicBezier {
    pub fn value_at(&self, t: f64) -> f64 {
        let s = 1.0 - t;
        self.a0 * s * s * s + 3.0 * self.a1 * t * s * s + 3.0 * self.a2 * t * t * s + self.a3 * t * t * t
    }
}

I don't think the definition of RustCubicBezier needs much explanation.

Rust functions work as methods if you define the first argument as & self.

Wrapper

This is the part that I don't really understand.

wrappable_struct!(RustCubicBezier, CubicBezierWrapper, CUBIC_BEZIER_WRAPPER);

It seems to show the relationship between the structure RustCubicBezier defined earlier and the wrapper.

The documentation for the wrappable_struct! macro is here: rutie :: wrappable_struct --Rust (Rutie 0.7.0 version)

(Addition 2020-10-01) The latest version of Rutie at the moment is 0.8.1, but for some reason the 0.8 version Failed to generate documents I'm leaving the link to the document in version 0.7.0 because the page doesn't exist.

I feel like it's somehow written to allow Rust structs to be wrapped in Ruby objects (I'm not good at English).

The first argument seems to give the name of the Rust struct you want to wrap. It seems that this structure must be public, so I added pub when I defined it earlier. The second argument seems to be the name of the structure (wrapper) for wrapping the first argument. The macro will automatically define this structure. However, in this code, the CubicBezierWrapper given as the second argument does not appear anywhere else. The third argument is the name of the [^ contain] static variable that contains the wrapper.

[^ contain]: Since it was "contain" in the original text, it was "included", but does it simply mean "have as a value" (~ is assigned)?

♪ Senior engineer who stopped learning
♪ You're dead
♪ The last chance to learn exciting
♪ Repeated slice Rust Chunk
♪ Scold me, demon compiler
♪ My head is already in trouble

No, rappers aren't like that [^ wrapper].

[^ wrapper]: I don't know about hip-hop music, so I don't know if the lyrics of rap are like this, but I wrote the lyrics to Tekito.

Class and method definitions

First class. This is simple.

class!(CubicBezier);

Then the method.

methods!(
    CubicBezier,
    rtself,

    fn cubic_bezier_new(a0: Float, a1: Float, a2: Float, a3: Float) -> CubicBezier {
        let a0 = a0.unwrap().to_f64();
        let a1 = a1.unwrap().to_f64();
        let a2 = a2.unwrap().to_f64();
        let a3 = a3.unwrap().to_f64();

        let rcb = RustCubicBezier{a0: a0, a1: a1, a2: a2, a3: a3};

        Class::from_existing("CubicBezier").wrap_data(rcb, &*CUBIC_BEZIER_WRAPPER)
    }

    fn value_at(t: Float) -> Float {
        let t = t.unwrap().to_f64();
        Float::new(rtself.get_data(&*CUBIC_BEZIER_WRAPPER).value_at(t))
    }
);

Click here for the definition of the methods! macro: https://docs.rs/rutie/0.7.0/src/rutie/dsl.rs.html#356-398

The meaning of the second argument of the methods! Macro, which I didn't understand last time, seemed to be vaguely understood (described later).

Two methods are defined here.

cubic_bezier_new creates an instance (this becomes new).

value_at calculates the value of the Bernstein function for t.

As I wrote last time, this function definition is an argument of the methods! Macro, and it is not a function of Rust as it is. It becomes a function definition of Rust by the function of the macro, but at that time I am doing it. If you're familiar with Rust macros, you'll probably understand it by looking at the links above.

In cubic_bezier_new, a structure of type RustCubicBezier to be wrapped is generated based on the argument. Of the last line

Class::from_existing("CubicBezier").wrap_data(rcb, &*CUBIC_BEZIER_WRAPPER)

But I'm not sure about this either. Class :: from_existing ("CubicBezier ") seems to get the CubicBezier class in short. Is it something like const_get ("CubicBezier ") in Ruby?

The documentation for wrap_data is here (read someday): rutie::Class - Rust

value_at is much easier to understand. Liver

rtself.get_data(&*CUBIC_BEZIER_WRAPPER)

By the way. At this point, the second argument rtself of themethods!Macro finally came out. This expression seems to return a wrapped RustCubicBezier struct. rtself is probably a self-like role in Ruby.

Definition of initialization function

The same note as last time. The "initialization function" is tentatively named by me and may not be appropriate.

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_cubic_bezier() {
    Class::new("CubicBezier", None).define(|klass| {
        klass.def_self("new", cubic_bezier_new);
        klass.def("value_at", value_at);
        klass.def("[]", value_at);
    });
}

ʻDefining a function that can be called from the outside, named Init_cubic_bezier`. Perhaps by doing this, you can actually create Ruby classes and methods.

It seems to use def_self for class methods (this is the same as last time) and def for instance methods. For value_at on the Rust side, value_at and [] are assigned on the Ruby side. Now CubicBezier # value_at andCubicBezier # []like aliases.

Hmmm, there are a lot of things I don't understand, but I managed to get the code on the Rust side by referring to the sample code. The code that "doesn't understand the principle but works by combining something that can be used somehow" is like a locomotive of 999 three nine </ ruby> [^ g999].

[^ g999]: The locomotive of Galaxy Express 999 that appears in "Galaxy Express 999" by Leiji Matsumoto is a technology by an unknown civilization discovered from the ruins of space (I do not know the contents, but I can use it somehow. ) Is combined. It must have been such a setting.

compile

In the project root directory

cargo build --release

Then you can create target / release / libmy_rutie_math.dylib. However, the extension is probably .so on Linux and .dll on Windows (.dylib is for macOS). This file is the only one used on the Ruby side.

Implementation: Ruby side

The code on the Rust side was a bit confusing, while the code on the Ruby side was pretty simple.

As before, the following code exists in the root directory of your Rust project. (If not, take the second argument of the ʻinitmethod (or thelib_path of Rutie.new`) as appropriate.

require "rutie"

Rutie.new(:cubic_bezier, lib_path: "target/release").init "Init_cubic_bezier", __dir__

cb = CubicBezier.new(1.0, 2.0, 1.5, 0.0)
0.0.step(1, by: 0.1) do |t|
  puts cb[t]
end

Now you can do cubic Bezier calculations in Ruby. Well, if you want to use it properly, don't wear it sideways as above, but put it in Gemfile

Gemfile


gem "rutie", "~> 0.0.4"

Write something like Bundle.require.

Bonus: Have a Bezier curve drawn

Now that you can calculate the Bezier curve, let's draw a picture. Use Cairo.

require "rutie"
require "cairo"

Rutie.new(:cubic_bezier, lib_path: "target/release").init "Init_cubic_bezier", __dir__

size = 400

surface = Cairo::ImageSurface.new Cairo::FORMAT_RGB24, size, size
context = Cairo::Context.new surface

context.rectangle 0, 0, size, size
context.set_source_color :white
context.fill

points = [[50, 100], [100, 300], [300, 350], [350, 50]]

bezier_x = CubicBezier.new(*points.map{ |x, _| x.to_f })
bezier_y = CubicBezier.new(*points.map{ |_, y| y.to_f })

context.set_source_color :gray
context.set_line_width 2
context.move_to(*points[0])
context.line_to(*points[1])
context.move_to(*points[2])
context.line_to(*points[3])
context.stroke

n = 100 #Division number
context.set_source_color :orange
(1...n).each do |i|
  t = i.fdiv(n)
  context.circle bezier_x[t], bezier_y[t], 1.5
  context.fill
end

context.set_source_color :red
points.each do |x, y|
  context.circle x, y, 4
  context.fill
end

surface.write_to_png "bezier.png "

Commentary is omitted (questions are welcome). I made a picture like this. bezier.png

The red dots are the four points that define the cubic Bezier curve. The gray line segment is a tangent vector. The small orange dots are the dots on the Bezier curve calculated by CubicBezier # []. Looking at this row of orange dots, you can see that "Oh, I can calculate something properly."

Benchmark test

Now it's time for the benchmark test. In the first place, one of the purposes of this attempt was to find an example of speeding up with Rust.

I will use benchmark_driver again, so if you haven't installed it yet

gem i benchmark_driver

Install with.

Test code

require "benchmark_driver"

Benchmark.driver do |r|
  r.prelude <<~PRELUDE
    require "rutie"
    Rutie.new(:cubic_bezier, lib_path: "target/release").init "Init_cubic_bezier", "#{__dir__}"

    class RubyCubicBezier
      def initialize(x0, x1, x2, x3)
        @x0, @x1, @x2, @x3 = x0, x1, x2, x3
      end

      def [](t)
        s = 1.0 - t
        @x0 * s * s * s + 3.0 * @x1 * t * s * s + 3.0 * @x2 * t * t * s + @x3 * t * t * t
      end
    end

    xs = [0.12, 0.48, 0.81, 0.95]
    rust_cubic_bezier = CubicBezier.new(*xs)
    ruby_cubic_bezier = RubyCubicBezier.new(*xs)
  PRELUDE

  r.report "rust_cubic_bezier[0.78]"
  r.report "ruby_cubic_bezier[0.78]"
end

Run this and compare the Ruby implementation with the Rust implementation. To tell you the truth, there is a good chance that the Rust version is slower because there is a cost to call Rust from Ruby.

Well, the result is:

rust_cubic_bezier[0.78]:   6731741.2 i/s
ruby_cubic_bezier[0.78]:   4733084.6 i/s - 1.42x  slower

Or, I won, the Rust version won! Hyahoi! !!

Well, it's about 1.4 times faster, so it's not a big deal. To be clear. However, I think it is significant to show that even such a (relatively simple) function can be accelerated with Rust. I also hoped that it could be achieved with dozens of lines of Rust code.

In the future, I'd like to let Rust do more processing and explore its practicality.

Recommended Posts

Ruby / Rust linkage (5) Numerical calculation with Rutie ② Bezier
Ruby / Rust linkage (4) Numerical calculation with Rutie
Ruby / Rust linkage (3) Numerical calculation with FFI
Ruby / Rust linkage (6) Extraction of morphemes
Object-oriented numerical calculation
Conditional numerical calculation