Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate R scripts #58

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions doc/api/r.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Integrate R code (:mod:`.util.r`)
*********************************

Developers working with :mod:`message_ix_models` can integrate existing R codes or use R to perform data processing related to building, running, and reporting models.
:mod:`message_ix_models.util.r` provides utility code to help with using :mod:`rpy2` to achieve this integration.

Two patterns are supported.
To use either, place a :file:`r_code.R` *in-line* in the source tree, either in :mod:`message_ix_models`, :mod:`message_data`, or another package::

message_ix_models/
project/
example/
__init__.py
module_a.py
module_b.py
r_code.R

The patterns are:

.. contents::
:local:
:backlinks: none


R ‘Modules’
===========

In this pattern, :func:`get_r_func` is used to source (i.e. import or run) R code from a file that defines one or more functions or other objects.
:func:`get_r_func` returns this function, which can then be called from Python code.
In this way, the R source file functions somewhat like a Python **‘module’**, while still being lighter weight than a full, installable R package.

.. code-block:: R
:caption: :file:`r_code.R`

# R file that defines functions and other entry points

mul <- function(x, y) {
# Multiply operands
return(x, y)
}

.. code-block:: python

from message_ix_models.util import get_r_func

# Source r_code.R, retrieve the function named "mul"
mul = get_r_func("message_ix_models.project.example.r_code:mul")

# Call the function
result = mul(1.2, 3.4)

# …use `result` in further Python code

.. currentmodule:: message_ix_models.util.r

.. autofunction:: get_r_func


Stand-alone scripts
===================

In this method, the R script is placed inline.
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Among other tasks, the tools allow modelers to:
api/project
api/tools
api/util
api/r
api/testing

.. toctree::
Expand Down
21 changes: 21 additions & 0 deletions message_ix_models/tests/r/module.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Example of an R 'module'
#
# This is a library of functions retrieved through
# message_ix_models.util.get_r_func()

add <- function(x, y) {
return(x + y)
}

mul <- function(x, y) {
return(x * y)
}

get_df <- function(x = 1.0) {
return (
data.frame(
node_loc = c("AT", "CA"),
value = x * c(1.2, 3.4)
)
)
}
13 changes: 13 additions & 0 deletions message_ix_models/tests/util/test_r.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from message_ix_models.util import get_r_func


def test_get_func():
"""R code can be sourced and called."""
get_df = get_r_func("tests.r.module:get_df")
get_df()

add = get_r_func("tests.r.module:add")
add(1.2, 3.4)

mul = get_r_func("tests.r.module:mul")
mul(1.2, 3.4)
2 changes: 2 additions & 0 deletions message_ix_models/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
private_data_path,
)
from .node import adapt_R11_R14, identify_nodes
from .r import get_r_func
from .scenarioinfo import ScenarioInfo

__all__ = [
Expand All @@ -30,6 +31,7 @@
"cached",
"check_support",
"convert_units",
"get_r_func",
"identify_nodes",
"load_package_data",
"load_private_data",
Expand Down
51 changes: 51 additions & 0 deletions message_ix_models/util/r.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Utilities for compatibility with R code."""
import re
from typing import Any

from .common import MESSAGE_DATA_PATH, MESSAGE_MODELS_PATH

_SOURCED = set()


def source_module(path):
from rpy2.robjects import r

path_parts = path.split(".")
package = (
path_parts.pop(0)
if re.match("message_(data|ix_models)", path_parts[0])
else "message_ix_models"
)

path = (
{
"message_data": MESSAGE_DATA_PATH,
"message_ix_models": MESSAGE_MODELS_PATH,
}[package]
.joinpath(*path_parts)
.with_suffix(".R")
)

if path not in _SOURCED:
_SOURCED.add(path)
r.source(str(path))


def get_r_func(path: str) -> Any:
"""Source R code and return an R function or other object.

Parameters
----------
path : str
Identifies the path to the R ‘module’ and the name of the object to be loaded.
"""
from rpy2.robjects import r

# Separate R code path and object name
path, name = path.rsplit(":", maxsplit=1)

# Source the R code
source_module(path)

# Retrieve and return the object
return r[name]
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ docs =
genno[compat]
sphinx >= 3.4.3
sphinx_rtd_theme
r =
rpy2
tests =
%(docs)s
%(r)s
pytest
pytest-cov

Expand Down Expand Up @@ -71,6 +74,8 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-pycountry]
ignore_missing_imports = True
[mypy-rpy2.*]
ignore_missing_imports = True
[mypy-setuptools]
ignore_missing_imports = True

Expand Down