Skip to content

Commit

Permalink
Merge b39ee53 into 7626a60
Browse files Browse the repository at this point in the history
  • Loading branch information
StrikerRUS committed May 8, 2020
2 parents 7626a60 + b39ee53 commit ac92e18
Show file tree
Hide file tree
Showing 18 changed files with 487 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -9,7 +9,7 @@ python:

env:
- TEST=API
- TEST=E2E LANG="c_lang or python or java or go_lang or javascript or php or haskell"
- TEST=E2E LANG="c_lang or python or java or go_lang or javascript or php or haskell or ruby"
- TEST=E2E LANG="c_sharp or visual_basic or powershell"
- TEST=E2E LANG="r_lang or dart"

Expand Down
6 changes: 6 additions & 0 deletions .travis/setup.sh
Expand Up @@ -43,3 +43,9 @@ if [[ $LANG == *"haskell"* ]]; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y haskell-platform
fi

# Install Ruby.
if [[ $LANG == *"ruby"* ]]; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y ruby-full
fi
3 changes: 2 additions & 1 deletion Dockerfile
Expand Up @@ -27,7 +27,8 @@ RUN apt-get update && \
r-base \
php \
dart \
haskell-platform && \
haskell-platform \
ruby-full && \
rm -rf /var/lib/apt/lists/*

WORKDIR /m2cgen
Expand Down
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -7,7 +7,7 @@
[![PyPI Version](https://img.shields.io/pypi/v/m2cgen.svg?logo=pypi&logoColor=white)](https://pypi.org/project/m2cgen)
[![Downloads](https://pepy.tech/badge/m2cgen)](https://pepy.tech/project/m2cgen)

**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP, Dart, Haskell).
**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP, Dart, Haskell, Ruby).

* [Installation](#installation)
* [Supported Languages](#supported-languages)
Expand Down Expand Up @@ -37,6 +37,7 @@ pip install m2cgen
- PowerShell
- Python
- R
- Ruby
- Visual Basic

## Supported Models
Expand Down
6 changes: 4 additions & 2 deletions m2cgen/__init__.py
Expand Up @@ -13,13 +13,14 @@
export_to_php,
export_to_dart,
export_to_haskell,
export_to_ruby,
)

__all__ = [
export_to_java,
export_to_python,
export_to_c,
export_to_go,
export_to_java,
export_to_python,
export_to_javascript,
export_to_visual_basic,
export_to_c_sharp,
Expand All @@ -28,6 +29,7 @@
export_to_php,
export_to_dart,
export_to_haskell,
export_to_ruby,
]

with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
Expand Down
1 change: 1 addition & 0 deletions m2cgen/cli.py
Expand Up @@ -34,6 +34,7 @@
"dart": (m2cgen.export_to_dart, ["indent", "function_name"]),
"haskell": (m2cgen.export_to_haskell,
["module_name", "indent", "function_name"]),
"ruby": (m2cgen.export_to_ruby, ["indent", "function_name"]),
}


Expand Down
24 changes: 24 additions & 0 deletions m2cgen/exporters.py
Expand Up @@ -354,6 +354,30 @@ def export_to_haskell(model, module_name="Model", indent=4,
return _export(model, interpreter)


def export_to_ruby(model, indent=4, function_name="score"):
"""
Generates a Ruby code representation of the given model.
Parameters
----------
model : object
The model object that should be transpiled into code.
indent : int, optional
The size of indents in the generated code.
function_name : string, optional
Name of the function in the generated code.
Returns
-------
code : string
"""
interpreter = interpreters.RubyInterpreter(
indent=indent,
function_name=function_name,
)
return _export(model, interpreter)


def _export(model, interpreter):
assembler_cls = assemblers.get_assembler_cls(model)
model_ast = assembler_cls(model).assemble()
Expand Down
2 changes: 2 additions & 0 deletions m2cgen/interpreters/__init__.py
Expand Up @@ -10,6 +10,7 @@
from .php.interpreter import PhpInterpreter
from .dart.interpreter import DartInterpreter
from .haskell.interpreter import HaskellInterpreter
from .ruby.interpreter import RubyInterpreter

__all__ = [
JavaInterpreter,
Expand All @@ -24,4 +25,5 @@
PhpInterpreter,
DartInterpreter,
HaskellInterpreter,
RubyInterpreter,
]
Empty file.
37 changes: 37 additions & 0 deletions m2cgen/interpreters/ruby/code_generator.py
@@ -0,0 +1,37 @@
import contextlib

from m2cgen.interpreters.code_generator import ImperativeCodeGenerator
from m2cgen.interpreters.code_generator import CodeTemplate as CT


class RubyCodeGenerator(ImperativeCodeGenerator):

tpl_var_declaration = CT("")
tpl_num_value = CT("${value}")
tpl_infix_expression = CT("(${left}) ${op} (${right})")
tpl_return_statement = tpl_num_value
tpl_array_index_access = CT("${array_name}[${index}]")
tpl_if_statement = CT("if ${if_def}")
tpl_else_statement = CT("else")
tpl_block_termination = CT("end")
tpl_var_assignment = CT("${var_name} = ${value}")

def add_function_def(self, name, args):
func_def = "def " + name + "("
func_def += ", ".join(args)
func_def += ")"
self.add_code_line(func_def)
self.increase_indent()

@contextlib.contextmanager
def function_definition(self, name, args):
self.add_function_def(name, args)
yield
self.add_block_termination()

def method_invocation(self, method_name, obj, args):
return ("(" + str(obj) + ")." + method_name +
"(" + ", ".join(map(str, args)) + ")")

def vector_init(self, values):
return "[" + ", ".join(values) + "]"
61 changes: 61 additions & 0 deletions m2cgen/interpreters/ruby/interpreter.py
@@ -0,0 +1,61 @@
import os

from m2cgen import ast
from m2cgen.interpreters import utils, mixins
from m2cgen.interpreters.ruby.code_generator import RubyCodeGenerator
from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter


class RubyInterpreter(ImperativeToCodeInterpreter,
mixins.LinearAlgebraMixin):

supported_bin_vector_ops = {
ast.BinNumOpType.ADD: "add_vectors",
}

supported_bin_vector_num_ops = {
ast.BinNumOpType.MUL: "mul_vector_number",
}

exponent_function_name = "Math.exp"
sqrt_function_name = "Math.sqrt"
tanh_function_name = "Math.tanh"

def __init__(self, indent=4, function_name="score", *args, **kwargs):
self.function_name = function_name

cg = RubyCodeGenerator(indent=indent)
super(RubyInterpreter, self).__init__(cg, *args, **kwargs)

def interpret(self, expr):
self._cg.reset_state()
self._reset_reused_expr_cache()

with self._cg.function_definition(
name=self.function_name,
args=[self._feature_array_name]):
last_result = self._do_interpret(expr)
self._cg.add_return_statement(last_result)

if self.with_linear_algebra:
filename = os.path.join(
os.path.dirname(__file__), "linear_algebra.rb")
self._cg.prepend_code_lines(utils.get_file_content(filename))

return self._cg.code

def interpret_bin_num_expr(self, expr, **kwargs):
if expr.op == ast.BinNumOpType.DIV:
# Always force float result
return self._cg.method_invocation(
method_name="fdiv",
obj=self._do_interpret(expr.left, **kwargs),
args=[self._do_interpret(expr.right, **kwargs)])
else:
return super().interpret_bin_num_expr(expr, **kwargs)

def interpret_pow_expr(self, expr, **kwargs):
base_result = self._do_interpret(expr.base_expr, **kwargs)
exp_result = self._do_interpret(expr.exp_expr, **kwargs)
return self._cg.infix_expression(
left=base_result, right=exp_result, op="**")
6 changes: 6 additions & 0 deletions m2cgen/interpreters/ruby/linear_algebra.rb
@@ -0,0 +1,6 @@
def add_vectors(v1, v2)
v1.zip(v2).map { |x, y| x + y }
end
def mul_vector_number(v1, num)
v1.map { |i| i * num }
end
2 changes: 2 additions & 0 deletions tests/e2e/executors/__init__.py
Expand Up @@ -10,6 +10,7 @@
from tests.e2e.executors.php import PhpExecutor
from tests.e2e.executors.dart import DartExecutor
from tests.e2e.executors.haskell import HaskellExecutor
from tests.e2e.executors.ruby import RubyExecutor

__all__ = [
JavaExecutor,
Expand All @@ -24,4 +25,5 @@
PhpExecutor,
DartExecutor,
HaskellExecutor,
RubyExecutor,
]
9 changes: 4 additions & 5 deletions tests/e2e/executors/haskell.py
Expand Up @@ -18,10 +18,9 @@
${print_code}
"""

EXECUTE_AND_PRINT_SCALAR = "print res"
PRINT_SCALAR = "print res"

EXECUTE_AND_PRINT_VECTOR = \
r"""mapM_ (putStr . \x -> show x ++ " ") res"""
PRINT_VECTOR = r"""mapM_ (putStr . \x -> show x ++ " ") res"""


class HaskellExecutor(base.BaseExecutor):
Expand All @@ -46,9 +45,9 @@ def predict(self, X):

def prepare(self):
if self.model_ast.output_size > 1:
print_code = EXECUTE_AND_PRINT_VECTOR
print_code = PRINT_VECTOR
else:
print_code = EXECUTE_AND_PRINT_SCALAR
print_code = PRINT_SCALAR
executor_code = string.Template(EXECUTOR_CODE_TPL).substitute(
executor_name=self.executor_name,
model_name=self.model_name,
Expand Down
8 changes: 4 additions & 4 deletions tests/e2e/executors/php.py
Expand Up @@ -19,11 +19,11 @@
${print_code}
"""

EXECUTE_AND_PRINT_SCALAR = """
PRINT_SCALAR = """
echo($res);
"""

EXECUTE_AND_PRINT_VECTOR = """
PRINT_VECTOR = """
echo(implode(" ", $res));
"""

Expand Down Expand Up @@ -53,9 +53,9 @@ def predict(self, X):

def prepare(self):
if self.model_ast.output_size > 1:
print_code = EXECUTE_AND_PRINT_VECTOR
print_code = PRINT_VECTOR
else:
print_code = EXECUTE_AND_PRINT_SCALAR
print_code = PRINT_SCALAR
executor_code = string.Template(EXECUTOR_CODE_TPL).substitute(
model_file=self.model_name,
print_code=print_code)
Expand Down
57 changes: 57 additions & 0 deletions tests/e2e/executors/ruby.py
@@ -0,0 +1,57 @@
import os
import string

from m2cgen import assemblers, interpreters
from tests import utils
from tests.e2e.executors import base

EXECUTOR_CODE_TPL = """
input_array = ARGV.map(&:to_f)
${model_code}
res = score(input_array)
${print_code}
"""

PRINT_SCALAR = """
puts res
"""

PRINT_VECTOR = """
puts res.join(" ")
"""


class RubyExecutor(base.BaseExecutor):
model_name = "score"

def __init__(self, model):
self.model = model
self.interpreter = interpreters.RubyInterpreter()

assembler_cls = assemblers.get_assembler_cls(model)
self.model_ast = assembler_cls(model).assemble()

self._ruby = "ruby"

def predict(self, X):
file_name = os.path.join(self._resource_tmp_dir,
"{}.rb".format(self.model_name))
exec_args = [self._ruby, file_name, *map(str, X)]
return utils.predict_from_commandline(exec_args)

def prepare(self):
if self.model_ast.output_size > 1:
print_code = PRINT_VECTOR
else:
print_code = PRINT_SCALAR
executor_code = string.Template(EXECUTOR_CODE_TPL).substitute(
model_code=self.interpreter.interpret(self.model_ast),
print_code=print_code)

file_name = os.path.join(
self._resource_tmp_dir, "{}.rb".format(self.model_name))
with open(file_name, "w") as f:
f.write(executor_code)
4 changes: 3 additions & 1 deletion tests/e2e/test_e2e.py
Expand Up @@ -15,7 +15,7 @@
from tests.e2e import executors


RECURSION_LIMIT = 5000
RECURSION_LIMIT = 5500


# pytest marks
Expand All @@ -31,6 +31,7 @@
PHP = pytest.mark.php
DART = pytest.mark.dart
HASKELL = pytest.mark.haskell
RUBY = pytest.mark.ruby
REGRESSION = pytest.mark.regr
CLASSIFICATION = pytest.mark.clf

Expand Down Expand Up @@ -143,6 +144,7 @@ def regression_bounded(model, test_fraction=0.02):
(executors.PhpExecutor, PHP),
(executors.DartExecutor, DART),
(executors.HaskellExecutor, HASKELL),
(executors.RubyExecutor, RUBY),
],
# These models will be tested against each language specified in the
Expand Down

0 comments on commit ac92e18

Please sign in to comment.