quickr

R to Fortran Transpiler

https://github.com/t-kalinowski/quickr

Science Score: 26.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
    Found .zenodo.json file
  • DOI references
  • Academic publication links
  • Academic email domains
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (15.4%) to scientific vocabulary
Last synced: 6 months ago · JSON representation

Repository

R to Fortran Transpiler

Basic Info
  • Host: GitHub
  • Owner: t-kalinowski
  • License: other
  • Language: R
  • Default Branch: main
  • Size: 1.83 MB
Statistics
  • Stars: 145
  • Watchers: 4
  • Forks: 5
  • Open Issues: 20
  • Releases: 2
Created about 1 year ago · Last pushed 6 months ago
Metadata Files
Readme Changelog License Agents

README.Rmd

---
output: github_document
editor_options:
  chunk_output_type: console
  markdown:
    wrap: sentence
---



```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.path = "man/figures/README-",
  out.width = "100%",
  fig.width=10
)
```

# quickr 





[![R-CMD-check](https://github.com/t-kalinowski/quickr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/t-kalinowski/quickr/actions/workflows/R-CMD-check.yaml)


The goal of quickr is to make your R code run quicker.

## Overview

R is an extremely flexible and dynamic language, but that flexibility and dynamicism can come at the expense of speed.
This package lets you trade back some of that flexibility for some speed, for the context of a single function.































The main exported function is `quick()`, here is how you use it.

```{r, eval = FALSE}
library(quickr)

convolve <- quick(function(a, b) {
  declare(type(a = double(NA)),
          type(b = double(NA)))
  ab <- double(length(a) + length(b) - 1)
  for (i in seq_along(a)) {
    for (j in seq_along(b)) {
      ab[i+j-1] <- ab[i+j-1] + a[i] * b[j]
    }
  }
  ab
})
```

`quick()` returns a quicker R function.
How much quicker?
Let's benchmark it!
For reference, we'll also compare it to a [pure-C](https://cran.r-project.org/doc/FAQ/R-exts.html#Calling-_002eCall-1) implementation.

```{r, message = FALSE, fig.width=10}
slow_convolve <- function(a, b) {
  declare(type(a = double(NA)),
          type(b = double(NA)))
  ab <- double(length(a) + length(b) - 1)
  for (i in seq_along(a)) {
    for (j in seq_along(b)) {
      ab[i+j-1] <- ab[i+j-1] + a[i] * b[j]
    }
  }
  ab
}

library(quickr)
quick_convolve <- quick(slow_convolve)

convolve_c <- inline::cfunction(
  sig = c(a = "SEXP", b = "SEXP"), body = r"({
    int na, nb, nab;
    double *xa, *xb, *xab;
    SEXP ab;

    a = PROTECT(Rf_coerceVector(a, REALSXP));
    b = PROTECT(Rf_coerceVector(b, REALSXP));
    na = Rf_length(a); nb = Rf_length(b); nab = na + nb - 1;
    ab = PROTECT(Rf_allocVector(REALSXP, nab));
    xa = REAL(a); xb = REAL(b); xab = REAL(ab);
    for(int i = 0; i < nab; i++) xab[i] = 0.0;
    for(int i = 0; i < na; i++)
        for(int j = 0; j < nb; j++)
            xab[i + j] += xa[i] * xb[j];
    UNPROTECT(3);
    return ab;
})")



a <- runif (100000); b <- runif (100)

timings <- bench::mark(
  r = slow_convolve(a, b),
  quickr = quick_convolve(a, b),
  c = convolve_c(a, b),
  min_time = 2
)
timings
plot(timings) + bench::scale_x_bench_time(base = NULL)
```

In the case of `convolve()`, `quick()` returns a function approximately *200* times quicker, giving similar performance to the C function.

`quick()` can accelerate any R function, with some restrictions:

-   Function arguments must have their types and shapes declared using `declare()`.
-   Only atomic vectors, matrices, and array are currently supported: `integer`, `double`, `logical`, and `complex`.
-   The return value must be an atomic array (e.g., not a list)
-   Named variables must have consistent shapes throughout their lifetimes.
-   `NA` values are not supported.
-   Only a subset of R's vocabulary is currently supported.

```{r, echo = FALSE}
quickr:::r2f_handlers |> names() |>
  (\(x) x[order(grepl("^[a-z]", x), grepl("^[A-Z]", x), x)])() |>
  noquote()
```

Many of these restrictions are expected to be relaxed as the project matures.
However, quickr is intended primarily for high-performance numerical computing, so features like polymorphic dispatch or support for complex or dynamic types are out of scope.

## `declare(type())` syntax:

The shape and mode of all function arguments must be declared.
Local and return variables may optionally also be declared.

`declare(type())` also has support for declaring size constraints, or size relationships between variables.
Here are some examples of declare calls:

```{r}
declare(type(x = double(NA))) # x is a 1-d double vector of any length
declare(type(x = double(10))) # x is a 1-d double vector of length 10
declare(type(x = double(1)))  # x is a scalar double

declare(type(x = integer(2, 3)))  # x is a 2-d integer matrix with dim (2, 3)
declare(type(x = integer(NA, 3))) # x is a 2-d integer matrix with dim (, 3)

# x is a 4-d logical matrix with dim (, 24, 24, 3)
declare(type(x = logical(NA, 24, 24, 3)))

# x and y are 1-d double vectors of any length
declare(type(x = double(NA)),
        type(y = double(NA)))

# x and y are 1-d double vectors of the same length
declare(
  type(x = double(n)),
  type(y = double(n)),
)

# x and y are 1-d double vectors, where length(y) == length(x) + 2
declare(type(x = double(n)),
        type(y = double(n+2)))
```

## More examples:

### `viterbi`

The Viterbi algorithm is an example of a dynamic programming algorithm within the family of Hidden Markov Models ().
Here, `quick()` makes the `viterbi()` approximately 50 times faster.

```{r}
slow_viterbi <- function(observations, states, initial_probs, transition_probs, emission_probs) {
    declare(
      type(observations = integer(num_steps)),
      type(states = integer(num_states)),
      type(initial_probs = double(num_states)),
      type(transition_probs = double(num_states, num_states)),
      type(emission_probs = double(num_states, num_obs)),
    )

    trellis <- matrix(0, nrow = length(states), ncol = length(observations))
    backpointer <- matrix(0L, nrow = length(states), ncol = length(observations))
    trellis[, 1] <- initial_probs * emission_probs[, observations[1]]

    for (step in 2:length(observations)) {
      for (current_state in 1:length(states)) {
        probabilities <- trellis[, step - 1] * transition_probs[, current_state]
        trellis[current_state, step] <- max(probabilities) * emission_probs[current_state, observations[step]]
        backpointer[current_state, step] <- which.max(probabilities)
      }
    }

    path <- integer(length(observations))
    path[length(observations)] <- which.max(trellis[, length(observations)])
    for (step in seq(length(observations) - 1, 1)) {
      path[step] <- backpointer[path[step + 1], step + 1]
    }

    out <- states[path]
    out
}

quick_viterbi <- quick(slow_viterbi)

set.seed(1234)
num_steps <- 16
num_states <- 8
num_obs <- 16

observations <- sample(1:num_obs, num_steps, replace = TRUE)
states <- 1:num_states
initial_probs <- runif (num_states)
initial_probs <- initial_probs / sum(initial_probs)  # normalize to sum to 1
transition_probs <- matrix(runif (num_states * num_states), nrow = num_states)
transition_probs <- transition_probs / rowSums(transition_probs)  # normalize rows
emission_probs <- matrix(runif (num_states * num_obs), nrow = num_states)
emission_probs <- emission_probs / rowSums(emission_probs)  # normalize rows

timings <- bench::mark(
  slow_viterbi = slow_viterbi(observations, states, initial_probs,
                              transition_probs, emission_probs),
  quick_viterbi = quick_viterbi(observations, states, initial_probs,
                                transition_probs, emission_probs)
)
timings
plot(timings)
```

### Diffusion simulation

Simulate how heat spreads over time across a 2D grid, using the [finite difference method](https://en.wikipedia.org/wiki/Finite_difference) applied to the [Heat Equation](https://en.wikipedia.org/wiki/Heat_equation).

Here, `quick()` returns a function over 100 times faster.

```{r}
diffuse_heat <- function(nx, ny, dx, dy, dt, k, steps) {
  declare(
    type(nx = integer(1)),
    type(ny = integer(1)),
    type(dx = integer(1)),
    type(dy = integer(1)),
    type(dt = double(1)),
    type(k = double(1)),
    type(steps = integer(1))
  )

  # Initialize temperature grid
  temp <- matrix(0, nx, ny)
  temp[nx / 2, ny / 2] <- 100  # Initial heat source in the center

  # Time stepping
  for (step in seq_len(steps)) {
    # Apply boundary conditions
    temp[1, ] <- 0
    temp[nx, ] <- 0
    temp[, 1] <- 0
    temp[, ny] <- 0

    # Update using finite differences
    temp_new <- temp
    for (i in 2:(nx - 1)) {
      for (j in 2:(ny - 1)) {
        temp_new[i, j] <- temp[i, j] + k * dt *
          ((temp[i + 1, j] - 2 * temp[i, j] + temp[i - 1, j]) /
             dx ^ 2 + (temp[i, j + 1] - 2 * temp[i, j] + temp[i, j - 1]) / dy ^ 2)
      }
    }
    temp <- temp_new

  }

  temp
}

quick_diffuse_heat <- quick(diffuse_heat)

# Parameters
nx <- 100L      # Grid size in x
ny <- 100L      # Grid size in y
dx <- 1L        # Grid spacing
dy <- 1L        # Grid spacing
dt <- 0.01      # Time step
k <- 0.1        # Thermal diffusivity
steps <- 500L   # Number of time steps

timings <- bench::mark(
  diffuse_heat = diffuse_heat(nx, ny, dx, dy, dt, k, steps),
  quick_diffuse_heat = quick_diffuse_heat(nx, ny, dx, dy, dt, k, steps)
)
summary(timings, relative = TRUE)
plot(timings)
```

### Rolling Mean

Here is quickr used to calculate a rolling mean.
Note that the CRAN package RcppRoll already provides a highly optimized rolling mean, which we include in the benchmarks for comparison.

```{r}
slow_roll_mean <- function(x, weights, normalize = TRUE) {
  declare(
    type(x = double(NA)),
    type(weights = double(NA)),
    type(normalize = logical(1))
  )
  out <- double(length(x) - length(weights) + 1)
  n <- length(weights)
  if (normalize)
    weights <- weights/sum(weights)*length(weights)

  for(i in seq_along(out)) {
    out[i] <- sum(x[i:(i+n-1)] * weights) / length(weights)
  }
  out
}

quick_roll_mean <- quick(slow_roll_mean)

x <- dnorm(seq(-3, 3, len = 100000))
weights <- dnorm(seq(-1, 1, len = 100))

timings <- bench::mark(
  r = slow_roll_mean(x, weights),
  rcpp = RcppRoll::roll_mean(x, weights = weights),
  quickr = quick_roll_mean(x, weights = weights)
)
timings

timings$expression <- factor(names(timings$expression), rev(names(timings$expression)))
plot(timings) + bench::scale_x_bench_time(base = NULL)
```

## Using `quickr` in an R package

When called in a package, `quick()` will pre-compile the quick functions and place them in the `./src` directory.
Run `devtools::load_all()` or `quickr::compile_package()` to ensure that the generated files in `./src` and `./R` are in sync with each other.

In a package, you must provide a function name to `quick()`. For example:

```r
my_fun <- quick(name = "my_fun", function(x) ....)
```

## Installation

You can install quickr from CRAN with:

``` r
install.packages("quickr")
```

You can install the development version of quickr from [GitHub](https://github.com/) with:

``` r
# install.packages("pak")
pak::pak("t-kalinowski/quickr")
```

You will also need a C and Fortran compiler, preferably the same ones used to build R itself.

On macOS:

-   Make sure xcode tools and gfortran are installed, as described in .
    In Terminal, run:

    ``` zsh
    sudo xcode-select --install
    # curl -LO https://mac.r-project.org/tools/gfortran-12.2-universal.pkg # R 4.4
    curl -LO https://mac.r-project.org/tools/gfortran-14.2-universal.pkg   # R 4.5
    sudo installer -pkg gfortran-12.2-universal.pkg -target /
    ```

On Windows:

-   Install the latest version of [Rtools](https://cran.r-project.org/bin/windows/Rtools/)

On Linux:

-   The "Install Required Dependencies" section [here](https://docs.posit.co/resources/install-r-source.html#install-required-dependencies) provides detailed instructions for installing R build tools on various Linux flavors.

Owner

  • Name: Tomasz Kalinowski
  • Login: t-kalinowski
  • Kind: user
  • Company: @posit-pbc

GitHub Events

Total
  • Create event: 13
  • Issues event: 29
  • Watch event: 107
  • Delete event: 3
  • Issue comment event: 39
  • Push event: 48
  • Public event: 1
  • Pull request review comment event: 6
  • Pull request review event: 4
  • Pull request event: 29
  • Fork event: 5
Last Year
  • Create event: 13
  • Issues event: 29
  • Watch event: 107
  • Delete event: 3
  • Issue comment event: 39
  • Push event: 48
  • Public event: 1
  • Pull request review comment event: 6
  • Pull request review event: 4
  • Pull request event: 29
  • Fork event: 5

Issues and Pull Requests

Last synced: 6 months ago

All Time
  • Total issues: 25
  • Total pull requests: 28
  • Average time to close issues: 3 days
  • Average time to close pull requests: about 9 hours
  • Total issue authors: 11
  • Total pull request authors: 4
  • Average comments per issue: 0.4
  • Average comments per pull request: 0.21
  • Merged pull requests: 21
  • Bot issues: 0
  • Bot pull requests: 0
Past Year
  • Issues: 25
  • Pull requests: 28
  • Average time to close issues: 3 days
  • Average time to close pull requests: about 9 hours
  • Issue authors: 11
  • Pull request authors: 4
  • Average comments per issue: 0.4
  • Average comments per pull request: 0.21
  • Merged pull requests: 21
  • Bot issues: 0
  • Bot pull requests: 0
Top Authors
Issue Authors
  • mns-nordicals (7)
  • t-kalinowski (6)
  • waynelapierre (3)
  • JoFrhwld (2)
  • Beliavsky (1)
  • ozjimbob (1)
  • sounkou-bioinfo (1)
  • tyner (1)
  • mikmart (1)
  • szimmer (1)
  • EmilHvitfeldt (1)
Pull Request Authors
  • t-kalinowski (22)
  • mns-nordicals (3)
  • shikokuchuo (2)
  • mikmart (1)
Top Labels
Issue Labels
Pull Request Labels
codex (3)

Packages

  • Total packages: 1
  • Total downloads:
    • cran 230 last-month
  • Total dependent packages: 0
  • Total dependent repositories: 0
  • Total versions: 2
  • Total maintainers: 1
cran.r-project.org: quickr

Compiler for R

  • Versions: 2
  • Dependent Packages: 0
  • Dependent Repositories: 0
  • Downloads: 230 Last month
Rankings
Dependent packages count: 26.4%
Dependent repos count: 32.5%
Average: 48.5%
Downloads: 86.6%
Maintainers (1)
Last synced: 6 months ago

Dependencies

DESCRIPTION cran
  • S7 * imports
  • dotty * imports
  • glue * imports
  • bench * suggests
  • pkgload >= 1.4.0.9000 suggests
  • rlang * suggests
  • testthat >= 3.0.0 suggests