rapl README

The project's README from github.com/JErnestoMtz/rapl. As one markdown file: README.md.

rapl

Documentation Crate

Enjoyable, composable, hackable N-dimensional arrays for Rust.

rapl combines the familiar parts of NumPy with a lot of inspiration from array programming languages like APL and BQN. Ranks are part of the type, so rank mistakes are compile errors.

rapl has a small core, and every operation can be built from a small set of composable primitives.

use rapl::*;

fn main() -> Result<(), DimError> {
    let x = Ndarr::from([3, 1, 4, 1, 5, 9, 2, 6]);

    let w = x.slice(s![Win(3)])?;          // every window of 3: a [6, 3] view, no copy
    println!("{w}");
    // ┌→────┐
    // ↓3 1 4│
    // │1 4 1│
    // │4 1 5│
    // │1 5 9│
    // │5 9 2│
    // │9 2 6│
    // └~────┘

    let peaks = w.reduce(1, i32::max)?;    // 4 4 5 9 9 9

    let hops = w.slice(s![..;2, ..])?;     // a stride is just a step on the positions
    // ┌→────┐
    // ↓3 1 4│
    // │4 1 5│
    // │5 9 2│
    // └~────┘

    // The same in 2-D: every 2×2 patch of an image, then max pooling.
    let img = Ndarr::from([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]);
    let pooled = img
        .slice(s![Win(2), Win(2)])?        // [2, 2, 3, 2]: row, kh, col, kw
        .reduce(3, i32::max)?
        .reduce(1, i32::max)?;
    // ┌→───────┐
    // ↓ 6  7  8│
    // │10 11 12│
    // └~───────┘
    Ok(())
}

rapl is in early development. The API is still settling, and speed is not yet a priority (see Philosophy), so it is not yet recommended for speed-sensitive applications.

What makes rapl different

Rank generic. Every operation works on arrays of any rank: a vector, a matrix, a batch of images, a stack of attention heads. The rank lives in the type (Ndarr<f32, U3>), so a rank mismatch is a compile error.

Type generic where possible. Elements are any T, not just floats: strings, chars, tuples, your own structs. Arithmetic needs arithmetic and sin needs a float. Everything else (slicing, permuting, mapping, folding, outer products) works for any element type.

Enjoyable, composable, hackable. rapl is not the fastest ndarray library in Rust and does not try to be. It tries to be pleasant to write, read and modify: a small core, three small dependencies, and conveniences defined as short compositions of public primitives rather than private kernels.

A taste

A 2-D convolution from windows and one fused contraction:

// image: [h, w, c_in], filters: [kh, kw, c_in, c_out]
let patches = image
    .slice(s![Win(3), Win(3), ..])?       // [h', 3, w', 3, c_in], a view
    .permute_axes(&[0, 2, 1, 3, 4])?;     // [h', w', 3, 3, c_in]
let features = patches.contract(&filters, U3::new(), |x, w| x * w, |a, b| a + b)?;

The examples include AlexNet with hand-written backpropagation, multi-head attention, an ultra-compact APL-style Conway's Game of Life, an Ising model, and FFT edge detection.

A tour

Snippets assume use rapl::*; and a function that returns Result, so ? works.

Creating arrays

let words = Ndarr::from(vec!["a", "b", "c"]);            // any element type
let matrix = Ndarr::from([[1, 2], [3, 4]]);              // rank from the literal
let range = Ndarr::from(1..7).reshape([2, 3])?;
let chars = Ndarr::from("Hello rapl!");                  // Ndarr<char, U1>

let zeros = Ndarr::<f64, U3>::zeros([2, 3, 4]);
let filled = Ndarr::fill("a", [5]);
let line = Ndarr::linspace(0.0, 1.0, 5);

// `from_fn` passes each element's coordinates:
let identity = Ndarr::from_fn([3, 3], |ix| i32::from(ix[0] == ix[1]));
// ┌→────┐
// ↓1 0 0│
// │0 1 0│
// │0 0 1│
// └~────┘

Random arrays use the same from_fn with a generator you own; rapl does not choose a PRNG.

Element-wise math and broadcasting

// Shapes broadcast from the right, as in NumPy, and complex numbers are native:
let a = Ndarr::from([1 + 1.i(), 2 + 1.i()]);
let b = Ndarr::from([[1, 2], [3, 4]]);
assert_eq!(a + b - 2, Ndarr::from([[1.i(), 2 + 1.i()], [2 + 1.i(), 4 + 1.i()]]));

let x = Ndarr::linspace(-1.0_f64, 1.0, 5);
let y = x.sin() * 2.0 + x.tanh();
let relu = x.map(|v| v.max(0.0));                 // anything else is one `map` away

// Combine two arrays, of different element types if needed:
let grid = Ndarr::from([[1, 2, 3], [4, 5, 6]]);
let labels = grid.zip_with(&Ndarr::from(vec!["a", "b", "c"]), |n, s| format!("{s}{n}"))?;
// ┌→───────┐
// ↓a1 b2 c3│
// │a4 b5 c6│
// └~───────┘

Indexing, slicing and views

let mut a = Ndarr::from([[0, 1, 2, 3], [4, 5, 6, 7]]);
assert_eq!(a[[1, 2]], 6);                          // fixed or dynamic rank alike

let row = a.slice(s![1, ..])?;                     // an integer drops the axis
let evens = a.slice(s![.., ..;2])?;                // ranges with steps
let reversed = a.slice(s![.., ..;-1])?;            // negative steps, as in NumPy
let start = 1usize;
let tail = a.slice(s![0, start..])?;               // any integer type, no casts

a.slice_mut(s![.., 0])?.map_in_place(|v| v * 10);  // mutable views, same syntax

Views are O(1): slice, t_view, windows and broadcasts share the original buffer. permute_axes and reshape consume their input and keep its storage, so they never copy; to_owned_array() is the explicit copy.

Reductions, scans and cells

use std::ops::Add;

let m = Ndarr::from([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let col_sums = m.reduce(0, f64::add)?;                  // [5, 7, 9], axis removed
let row_max = m.reduce(Keep(1), f64::max)?;             // [[3], [6]], axis kept
let centered = &m - &row_max;                           // so it broadcasts back
let running = m.scan_axis(1, ScanDirection::Forward, |acc, x| acc + x)?;

// Cells: any function over the trailing axes, e.g. the max of each window.
let windows = m.slice(s![Win(2), Win(2)])?.permute_axes(&[0, 2, 1, 3])?;
let window_max = windows.map_cells(U2::new(), |w| {
    w.iter_elems().copied().fold(f64::NEG_INFINITY, f64::max)
})?;

Products

let a = Ndarr::from(1..7).reshape([2, 3])?;
let b = Ndarr::from(1..7).reshape([3, 2])?;
let product = a.mat_mul(&b)?;

// `mat_mul` batches leading axes like NumPy's `@`:
let batch = Ndarr::from(1..13).reshape([2, 2, 3])?;
assert_eq!(batch.mat_mul(&b)?.shape(), &[2, 2, 2]);

// The general form: any number of contracted axes, any combining functions.
let tensordot = a.contract(&b, U1::new(), |x, y| x * y, |x, y| x + y)?;

// Outer products combine every pair, for any element type:
let suits = Ndarr::from(vec!["♣", "♠", "♥", "♦"]);
let ranks = Ndarr::from(vec!["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]);
let deck = ranks.outer_product(&suits, |rank, suit| format!("{rank}{suit}"))?;
assert_eq!(deck.len(), 52);
println!("{deck}");
// ┌→──────────────┐
// ↓2♣  2♠  2♥  2♦ │
// │3♣  3♠  3♥  3♦ │
// │4♣  4♠  4♥  4♦ │
// │5♣  5♠  5♥  5♦ │
// │6♣  6♠  6♥  6♦ │
// │7♣  7♠  7♥  7♦ │
// │8♣  8♠  8♥  8♦ │
// │9♣  9♠  9♥  9♦ │
// │10♣ 10♠ 10♥ 10♦│
// │J♣  J♠  J♥  J♦ │
// │Q♣  Q♠  Q♥  Q♦ │
// │K♣  K♠  K♥  K♦ │
// │A♣  A♠  A♥  A♦ │
// └~──────────────┘

Rank checking, static and dynamic

let m = Ndarr::from([[1, 2], [3, 4]]);
let v: Ndarr<i32, U1> = m.reduce(0, |a, b| a + b)?;   // U2 -> U1, checked at compile time
let [rows, cols] = m.dim().to_array();               // destructure a fixed-rank shape

let d = m.clone().into_dyn();                         // rank known only at runtime
let w: Ndarr<i32, Dyn> = d.reduce(0, |a, b| a + b)?;  // same API, checked at runtime

Complex numbers and FFT

let z = 1 + 2.i();
assert_eq!(z - 3, -2 + 2.i());

let shifted = Ndarr::from([1, 2, 3]) + -1 + 2.i();    // complex arrays
assert_eq!(shifted.im(), Ndarr::from([2, 2, 2]));

// 1-D and 2-D FFT with the `fft` feature, implemented in rapl itself:
let signal = Ndarr::linspace(-10.0, 10.0, 100).sin();
let spectrum = signal.to_complex().fft();

Philosophy

Plotting

rapl-plot draws Ndarrs directly, to SVG or PNG. The shape of the data decides what is drawn, and labels, colors and widths broadcast like any other array.

use rapl::*;
use rapl_plot::*;

let x = Ndarr::linspace(0.0, 10.0, 200);
// [200, 2]: one curve per column.
let waves = Ndarr::from_fn([200, 2], |ix| (x[[ix[0]]] + ix[1] as f64).sin());

line(&x, &waves).label(["sin(x)", "sin(x + 1)"]).save("waves.svg")?;

Getting started

cargo add rapl
# or, with the FFT:
cargo add rapl --features fft

Complex numbers are enabled by default. Contributions and issues are welcome.

rapl (JErnestoMtz/rapl)

Searching Database…

106 ·GitHub
这是什么?rapl 是一个用于 N 维数组(ndarray)的 Rust 库,它把数组的秩(rank)编码进类型里,秩不匹配就会变成编译错误。它结合了 NumPy 以及 APL、BQN 等数组语言的思想。
它是一个库还是独立工具?它是一个 Rust 库(crate),作为依赖添加到你自己的 Rust 项目中;还有一个配套的 rapl-plot crate,用来对数组做可视化。
我怎么把它加到 Rust 项目里?把 rapl 添加为依赖,再用 'use rapl::*;' 导入,就可以使用它的数组创建和操作函数了。
rapl 在运行时依赖什么?只有三个小 crate:用于编译期秩系统的 typenum 和 generic-array,以及提供数值 trait 的 num-traits;像 FFT 这样的功能是 rapl 自己实现的。
它适合对性能要求高的场景吗?不适合。API 还在变动中,也没有 SIMD 内核或 BLAS 绑定;它优先考虑清晰性而不是速度,目前不推荐用于对速度敏感的应用。
rapl 是给谁用的?它面向的是用数组做探索、原型开发、教学和脚本编写,而不是需要极致速度的重度生产负载。
rapl 能处理字符串这类非数值的元素类型吗?可以。rapl 是泛型的:元素可以是任意类型 T,包括字符串、字符、元组或你自己的结构体。切片、重排轴、映射和折叠等操作适用于任何元素类型,而涉及算术运算的操作则要求元素支持算术。
rapl 自带了哪些示例?示例包括带手写反向传播的 AlexNet、多头注意力、APL 风格的康威生命游戏、Ising 模型,以及基于 FFT 的边缘检测。
数组视图会复制数据吗?不会。slice、tview、windows 和广播等操作都是共享原始缓冲区的 O(1) 视图;permuteaxes 和 reshape 通过消耗输入来避免复制,只有显式调用 toownedarray() 时才会复制。
内置复数和 FFT 支持吗?支持。复数功能默认开启,FFT 也是在 rapl 内部实现的,不依赖外部库。
Size
5.4 MB
Stars
106
Age
3y
Commits · 90 Days