Skip to content

Commit

Permalink
Merge branch 'master' into str_format
Browse files Browse the repository at this point in the history
  • Loading branch information
StrikerRUS committed May 14, 2020
2 parents 79b96f9 + 3db1cc2 commit 45c6fe7
Show file tree
Hide file tree
Showing 22 changed files with 537 additions and 40 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
8 changes: 7 additions & 1 deletion .travis/setup.sh
Expand Up @@ -7,7 +7,7 @@ if [[ $LANG == *"c_sharp"* ]] || [[ $LANG == *"visual_basic"* ]]; then
wget -q https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install --no-install-recommends -y dotnet-sdk-3.0
sudo apt-get install --no-install-recommends -y dotnet-sdk-3.1
fi

# Install PowerShell Core.
Expand Down 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
5 changes: 3 additions & 2 deletions Dockerfile
Expand Up @@ -22,12 +22,13 @@ RUN apt-get update && \
python3.7-dev \
openjdk-8-jdk \
golang-go \
dotnet-sdk-3.0 \
dotnet-sdk-3.1 \
powershell \
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
20 changes: 18 additions & 2 deletions m2cgen/ast.py
@@ -1,4 +1,6 @@
from enum import Enum
from inspect import getmembers, isclass
from sys import modules


class Expr:
Expand All @@ -11,6 +13,17 @@ class Expr:
to_reuse = False


class IdExpr(Expr):
def __init__(self, expr, to_reuse=False):
self.expr = expr
self.to_reuse = to_reuse
self.output_size = expr.output_size

def __str__(self):
args = ",".join([str(self.expr), "to_reuse=" + str(self.to_reuse)])
return "IdExpr(" + args + ")"


class FeatureRef(Expr):
def __init__(self, index):
self.index = index
Expand Down Expand Up @@ -129,7 +142,7 @@ class VectorExpr(Expr):
class VectorVal(VectorExpr):

def __init__(self, exprs):
assert all(map(lambda e: e.output_size == 1, exprs)), (
assert all(e.output_size == 1 for e in exprs), (
"All expressions for VectorVal must be scalar")

self.exprs = exprs
Expand Down Expand Up @@ -230,12 +243,15 @@ def __str__(self):
return "IfExpr(" + args + ")"


TOTAL_NUMBER_OF_EXPRESSIONS = len(getmembers(modules[__name__], isclass))


NESTED_EXPRS_MAPPINGS = [
((BinExpr, CompExpr), lambda e: [e.left, e.right]),
(PowExpr, lambda e: [e.base_expr, e.exp_expr]),
(VectorVal, lambda e: e.exprs),
(IfExpr, lambda e: [e.test, e.body, e.orelse]),
((ExpExpr, SqrtExpr, TanhExpr), lambda e: [e.expr]),
((IdExpr, ExpExpr, SqrtExpr, TanhExpr), lambda e: [e.expr]),
]


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,
]
18 changes: 5 additions & 13 deletions m2cgen/interpreters/interpreter.py
@@ -1,7 +1,5 @@
import re

from m2cgen import ast
from m2cgen.interpreters.utils import CachedResult
from m2cgen.interpreters.utils import CachedResult, _get_handler_name


class BaseInterpreter:
Expand Down Expand Up @@ -55,21 +53,12 @@ def _reset_reused_expr_cache(self):
self._cached_expr_results = {}

def _select_handler(self, expr):
handler_name = self._handler_name(type(expr))
handler_name = _get_handler_name(type(expr))
if hasattr(self, handler_name):
return getattr(self, handler_name)
raise NotImplementedError(
"No handler found for '{}'".format(type(expr).__name__))

@staticmethod
def _handler_name(expr_tpe):
expr_name = BaseInterpreter._normalize_expr_name(expr_tpe.__name__)
return "interpret_" + expr_name

@staticmethod
def _normalize_expr_name(name):
return re.sub("(?!^)([A-Z]+)", r"_\1", name).lower()


class BaseToCodeInterpreter(BaseInterpreter):

Expand Down Expand Up @@ -101,6 +90,9 @@ def __init__(self, cg, feature_array_name="input"):
self.with_vectors = False
self.with_math_module = False

def interpret_id_expr(self, expr, **kwargs):
return self._do_interpret(expr.expr, **kwargs)

def interpret_comp_expr(self, expr, **kwargs):
op = self._cg._comp_op_overwrite(expr.op)
return self._cg.infix_expression(
Expand Down
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
16 changes: 16 additions & 0 deletions m2cgen/interpreters/utils.py
@@ -1,4 +1,10 @@
import re

from collections import namedtuple
from functools import lru_cache
from math import ceil, log

from m2cgen.ast import TOTAL_NUMBER_OF_EXPRESSIONS


CachedResult = namedtuple('CachedResult', ['var_name', 'expr_result'])
Expand All @@ -7,3 +13,13 @@
def get_file_content(path):
with open(path) as f:
return f.read()


@lru_cache(maxsize=1 << ceil(log(TOTAL_NUMBER_OF_EXPRESSIONS, 2)))
def _get_handler_name(expr_tpe):
expr_name = _normalize_expr_name(expr_tpe.__name__)
return "interpret_" + expr_name


def _normalize_expr_name(name):
return re.sub("(?!^)([A-Z]+)", r"_\1", name).lower()
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,
]

0 comments on commit 45c6fe7

Please sign in to comment.