Skip to content

Commit

Permalink
Adding Javascript interpreter to support scoring in BigQuery. (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcampbell-prosper authored and izeigerman committed Sep 20, 2019
1 parent 39ea6e6 commit d3a4760
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 2 deletions.
2 changes: 2 additions & 0 deletions m2cgen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
export_to_go,
export_to_java,
export_to_python,
export_to_javascript,
)

__all__ = [
export_to_java,
export_to_python,
export_to_c,
export_to_go,
export_to_javascript,
]
2 changes: 2 additions & 0 deletions m2cgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
m2cgen.export_to_java, ["indent", "class_name", "package_name"]),
"c": (m2cgen.export_to_c, ["indent"]),
"go": (m2cgen.export_to_go, ["indent"]),
"javascript": (
m2cgen.export_to_javascript, ["indent"]),
}


Expand Down
19 changes: 19 additions & 0 deletions m2cgen/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ def export_to_go(model, indent=4):
return _export(model, interpreter)


def export_to_javascript(model, indent=4):
"""
Generates a Javascript 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.
Returns
-------
code : string
"""
interpreter = interpreters.JavascriptInterpreter(indent=indent)
return _export(model, interpreter)


def _export(model, interpreter):
assembler_cls = assemblers.get_assembler_cls(model)
model_ast = assembler_cls(model).assemble()
Expand Down
3 changes: 2 additions & 1 deletion m2cgen/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
from .python.interpreter import PythonInterpreter
from .c.interpreter import CInterpreter
from .go.interpreter import GoInterpreter

from .javascript.interpreter import JavascriptInterpreter

__all__ = [
JavaInterpreter,
PythonInterpreter,
CInterpreter,
GoInterpreter,
JavascriptInterpreter,
]
Empty file.
27 changes: 27 additions & 0 deletions m2cgen/interpreters/javascript/code_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import contextlib

from m2cgen.interpreters.code_generator import CLikeCodeGenerator


class JavascriptCodeGenerator(CLikeCodeGenerator):
def __init__(self, *args, **kwargs):
super(JavascriptCodeGenerator, self).__init__(*args, **kwargs)

def add_function_def(self, name, args):
function_def = "function " + name + "("
function_def += ",".join([n for is_vector, n in args])
function_def += ") {"
self.add_code_line(function_def)
self.increase_indent()

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

def vector_init(self, values):
return "[" + ", ".join(values) + "]"

def _get_var_declare_type(self, is_vector):
return "var"
50 changes: 50 additions & 0 deletions m2cgen/interpreters/javascript/interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os

from m2cgen import ast
from m2cgen.interpreters import mixins
from m2cgen.interpreters import utils
from m2cgen.interpreters.interpreter import ToCodeInterpreter
from m2cgen.interpreters.javascript.code_generator \
import JavascriptCodeGenerator


class JavascriptInterpreter(ToCodeInterpreter,
mixins.LinearAlgebraMixin):

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

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

exponent_function_name = "Math.exp"
power_function_name = "Math.pow"
tanh_function_name = "Math.tanh"

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

cg = JavascriptCodeGenerator(indent=indent)
super(JavascriptInterpreter, self).__init__(cg, *args, **kwargs)

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

args = [(True, self._feature_array_name)]

with self._cg.function_definition(
name="score",
args=args):
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.js")
self._cg.add_code_lines(utils.get_file_content(filename))

return self._cg.code
19 changes: 19 additions & 0 deletions m2cgen/interpreters/javascript/linear_algebra.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function addVectors(v1, v2) {
let result = new Array(v1.length);

for (let i = 0; i < v1.length; i++) {
result[i] = v1[i] + v2[i];
}

return result;
}

function mulVectorNumber(v1, num) {
let result = new Array(v1.length);

for (let i = 0; i < v1.length; i++) {
result[i] = v1[i] * num;
}

return result;
}
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pytest==4.1.1
pytest-mock==1.10.0
coveralls==1.5.1
pytest-cov==2.6.1
py-mini-racer==0.1.18
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
license="MIT",
packages=find_packages(exclude=["tests.*", "tests"]),
package_data={
"": ["linear_algebra.java", "linear_algebra.c", "linear_algebra.go"],
"": ["linear_algebra.java", "linear_algebra.c", "linear_algebra.go",
"linear_algebra.js"],
},
classifiers=[
"Development Status :: 3 - Alpha",
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/executors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from tests.e2e.executors.python import PythonExecutor
from tests.e2e.executors.c import CExecutor
from tests.e2e.executors.go import GoExecutor
from tests.e2e.executors.javascript import JavascriptExecutor

__all__ = [
JavaExecutor,
PythonExecutor,
CExecutor,
GoExecutor,
JavascriptExecutor,
]
31 changes: 31 additions & 0 deletions tests/e2e/executors/javascript.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os
from py_mini_racer import py_mini_racer
import m2cgen as m2c
from tests.e2e.executors import base


class JavascriptExecutor(base.BaseExecutor):

def __init__(self, model):
self.model = model

def predict(self, X):
file_name = os.path.join(self._resource_tmp_dir, "model.js")

with open(file_name, 'r') as myfile:
code = myfile.read()

caller = "score([" + ",".join(map(str, X)) + "]);\n"

ctx = py_mini_racer.MiniRacer()
result = ctx.eval(caller + code)

return result

def prepare(self):
code = m2c.export_to_javascript(self.model)

file_name = os.path.join(self._resource_tmp_dir, "model.js")

with open(file_name, "w") as f:
f.write(code)
2 changes: 2 additions & 0 deletions tests/e2e/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
JAVA = pytest.mark.java
C = pytest.mark.c
GO = pytest.mark.go
JAVASCRIPT = pytest.mark.javascript
REGRESSION = pytest.mark.regr
CLASSIFICATION = pytest.mark.clf

Expand Down Expand Up @@ -69,6 +70,7 @@ def classification_binary(model):
(executors.JavaExecutor, JAVA),
(executors.CExecutor, C),
(executors.GoExecutor, GO),
(executors.JavascriptExecutor, JAVASCRIPT),
],
# These models will be tested against each language specified in the
Expand Down

0 comments on commit d3a4760

Please sign in to comment.