Skip to content

Commit

Permalink
Merge branch 'master' into benchmarking2
Browse files Browse the repository at this point in the history
  • Loading branch information
enekomartinmartinez committed Aug 9, 2021
2 parents f9d18d5 + de7742c commit 2154a5f
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/development/supported_vensim_functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
+------------------------------+------------------------------+
| PROD | functions.prod |
+------------------------------+------------------------------+
| INVERT MATRIX | functions.invert_matrix |
+------------------------------+------------------------------+
| GET XLS DATA | external.ExtData |
+------------------------------+------------------------------+
| GET DIRECT DATA | external.ExtData |
Expand Down
32 changes: 26 additions & 6 deletions pysd/py_backend/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1966,7 +1966,7 @@ def random_0_1():
-------
A random number from the uniform distribution between 0 and 1.
"""
return np.random.uniform(0,1)
return np.random.uniform(0, 1)


def random_uniform(m, x, s):
Expand Down Expand Up @@ -2011,7 +2011,7 @@ def not_implemented_function(*args):

def log(x, base):
"""
Implements Vensim's LOG function with change of base
Implements Vensim's LOG function with change of base.
Parameters
----------
Expand All @@ -2028,7 +2028,7 @@ def log(x, base):

def sum(x, dim=None):
"""
Implements Vensim's SUM function
Implements Vensim's SUM function.
Parameters
----------
Expand All @@ -2053,7 +2053,7 @@ def sum(x, dim=None):

def prod(x, dim=None):
"""
Implements Vensim's PROD function
Implements Vensim's PROD function.
Parameters
----------
Expand All @@ -2078,7 +2078,7 @@ def prod(x, dim=None):

def vmin(x, dim=None):
"""
Implements Vensim's Vmin function
Implements Vensim's Vmin function.
Parameters
----------
Expand All @@ -2103,7 +2103,7 @@ def vmin(x, dim=None):

def vmax(x, dim=None):
"""
Implements Vensim's VMAX function
Implements Vensim's VMAX function.
Parameters
----------
Expand All @@ -2124,3 +2124,23 @@ def vmax(x, dim=None):
return float(x.max())

return x.max(dim=dim)


def invert_matrix(mat):
"""
Implements Vensim's INVERT MATRIX function.
Invert the matrix defined by the last two dimensions of xarray.DataArray.
Paramteters
-----------
mat: xarray.DataArray
The matrix to invert.
Returns
-------
mat1: xarray.DataArray
Inverted matrix.
"""
return xr.DataArray(np.linalg.inv(mat.values), mat.coords, mat.dims)
34 changes: 26 additions & 8 deletions pysd/py_backend/vensim/vensim2py.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,18 +709,21 @@ def parse_units(units_str):
"prod": {"name": "prod", "module": "functions"},
"vmin": {"name": "vmin", "module": "functions"},
"vmax": {"name": "vmax", "module": "functions"},
# matricial functions
"invert matrix": {
"name": "invert_matrix",
"parameters": [
{"name": "mat"}
# we can safely ignore VENSIM's n parameter
],
"module": "functions"},
# TODO functions/stateful objects to be added
# https://github.com/JamesPHoughton/pysd/issues/154
"forecast": {
"name": "not_implemented_function",
"module": "functions",
"original_name": "FORECAST",
},
"invert matrix": {
"name": "not_implemented_function",
"module": "functions",
"original_name": "INVERT MATRIX",
},
"get time value": {
"name": "not_implemented_function",
"module": "functions",
Expand Down Expand Up @@ -1016,10 +1019,11 @@ def parse_general_expression(element, namespace={}, subscript_dict={},
r"""
expr_type = array / expr / empty
expr = _ pre_oper? _ (lookup_with_def / build_call / macro_call / call / lookup_call / parens / number / string / reference) _ (in_oper _ expr)?
subs_expr = subs _ in_oper _ subs
logical_expr = logical_in_expr / logical_pre_expr / logical_parens
logical_in_expr = (logical_pre_expr / logical_parens / expr) (_ in_logical_oper _ (logical_pre_expr / logical_parens / expr))+
logical_pre_expr = pre_logical_oper _ (logical_parens / expr)
logical_expr = logical_in_expr / logical_pre_expr / logical_parens / subs_expr
logical_in_expr = (logical_pre_expr / logical_parens / subs_expr / expr) (_ in_logical_oper _ (logical_pre_expr / logical_parens / subs_expr / expr))+
logical_pre_expr = pre_logical_oper _ (logical_parens / subs_expr / expr)
lookup_with_def = ~r"(WITH\ LOOKUP)"I _ "(" _ expr _ "," _ "(" _ ("[" ~r"[^\]]*" "]" _ ",")? ( "(" _ expr _ "," _ expr _ ")" _ ","? _ )+ _ ")" _ ")"
Expand Down Expand Up @@ -1279,6 +1283,20 @@ def visit_array(self, n, vc):
else:
return n.text.replace(" ", "")

def visit_subs_expr(self, n, vc):
# visit a logical comparation between subscripts
return builder.build_function_call(
functions_utils["DataArray"], [
f"_subscript_dict['{vc[0]}']",
"{"+f"'{vc[0]}': _subscript_dict['{vc[0]}']"+"}",
f"'{vc[0]}'"]
) + vc[2] + builder.build_function_call(
functions_utils["DataArray"], [
f"_subscript_dict['{vc[4]}']",
"{"+f"'{vc[4]}': _subscript_dict['{vc[4]}']"+"}",
f"'{vc[4]}'"]
)

def visit_subscript_list(self, n, vc):
refs = vc[4]
subs = [x.strip() for x in refs.split(",")]
Expand Down
8 changes: 8 additions & 0 deletions tests/integration_test_vensim_pathway.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ def test_input_functions(self):
output, canon = runner(test_models + '/input_functions/test_inputs.mdl')
assert_frames_close(output, canon, rtol=rtol)

def test_invert_matrix(self):
output, canon = runner(test_models + '/invert_matrix/test_invert_matrix.mdl')
assert_frames_close(output, canon, rtol=rtol)

def test_limits(self):
output, canon = runner(test_models + '/limits/test_limits.mdl')
assert_frames_close(output, canon, rtol=rtol)
Expand Down Expand Up @@ -325,6 +329,10 @@ def test_subrange_merge(self):
output, canon = runner(test_models + '/subrange_merge/test_subrange_merge.mdl')
assert_frames_close(output, canon, rtol=rtol)

def test_subscript_logicals(self):
output, canon = runner(test_models + '/subscript_logicals/test_subscript_logicals.mdl')
assert_frames_close(output, canon, rtol=rtol)

def test_subscript_multiples(self):
output, canon = runner(test_models + '/subscript_multiples/test_multiple_subscripts.mdl')
assert_frames_close(output, canon, rtol=rtol)
Expand Down
3 changes: 2 additions & 1 deletion tests/unit_test_benchmarking.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ def test_different_frames_warning(self):
with catch_warnings(record=True) as ws:
assert_frames_close(
load_outputs(os.path.join(_root, "data/out_teacup.csv")),
load_outputs(os.path.join(_root, "data/out_teacup_modified.csv")),
load_outputs(
os.path.join(_root, "data/out_teacup_modified.csv")),
assertion="warn", verbose=True)

# use only user warnings
Expand Down
49 changes: 49 additions & 0 deletions tests/unit_test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,55 @@ def test_vmax(self):
self.assertEqual(pysd.functions.vmax(data, dim=['d1', 'd2']), 4)
self.assertEqual(pysd.functions.vmax(data), 4)

def test_invert_matrix(self):
"""
Test for invert_matrix function
"""
import pysd

coords1 = {'d1': ['a', 'b'], 'd2': ['a', 'b']}
coords2 = {'d0': ['1', '2'], 'd1': ['a', 'b'], 'd2': ['a', 'b']}
coords3 = {'d0': ['1', '2'],
'd1': ['a', 'b', 'c'],
'd2': ['a', 'b', 'c']}

data1 = xr.DataArray([[1, 2], [3, 4]], coords1, ['d1', 'd2'])
data2 = xr.DataArray([[[1, 2], [3, 4]], [[-1, 2], [5, 4]]],
coords2,
['d0', 'd1', 'd2'])
data3 = xr.DataArray([[[1, 2, 3], [3, 7, 2], [3, 4, 6]],
[[-1, 2, 3], [4, 7, 3], [5, 4, 6]]],
coords3,
['d0', 'd1', 'd2'])

for data in [data1, data2, data3]:
datai = pysd.functions.invert_matrix(data)
self.assertEqual(data.dims, datai.dims)

if len(data.shape) == 2:
# two dimensions xarrays
self.assertTrue((
abs(np.dot(data, datai) - np.dot(datai, data))
< 1e-14
).all())
self.assertTrue((
abs(np.dot(data, datai) - np.identity(data.shape[-1]))
< 1e-14
).all())
else:
# three dimensions xarrays
for i in range(data.shape[0]):
self.assertTrue((
abs(np.dot(data[i], datai[i])
- np.dot(datai[i], data[i]))
< 1e-14
).all())
self.assertTrue((
abs(np.dot(data[i], datai[i])
- np.identity(data.shape[-1]))
< 1e-14
).all())

def test_incomplete(self):
import pysd
from warnings import catch_warnings
Expand Down
51 changes: 51 additions & 0 deletions tests/unit_test_vensim2py.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,57 @@ def test_subscript_ranges(self):
res[0]["py_expr"], "rearrange(var_c(),['Range1'],_subscript_dict)"
)

def test_invert_matrix(self):
from pysd.py_backend.vensim.vensim2py import parse_general_expression

res = parse_general_expression(
{
"expr": "INVERT MATRIX(A, 3)",
"real_name": "A1",
"py_name": "a",
},
{
"A": "a",
"A1": "a1",
},
subscript_dict={
"dim1": ["a", "b", "c"], "dim2": ["a", "b", "c"]
},
elements_subs_dict={
"a1": ["dim1", "dim2"],
"a": ["dim1", "dim2"]
}
)

self.assertEqual(res[0]["py_expr"], "invert_matrix(a())")

def test_subscript_logicals(self):
from pysd.py_backend.vensim.vensim2py import parse_general_expression

res = parse_general_expression(
{
"expr": "IF THEN ELSE(dim1=dim2, 5, 0)",
"real_name": "A",
"py_name": "a",
},
{
"A": "a",
},
subscript_dict={
"dim1": ["a", "b", "c"], "dim2": ["a", "b", "c"]
},
elements_subs_dict={
"a": ["dim1", "dim2"]
}
)

self.assertIn(
"xr.DataArray(_subscript_dict['dim1'],"
"{'dim1': _subscript_dict['dim1']},'dim1')"
"==xr.DataArray(_subscript_dict['dim2'],"
"{'dim2': _subscript_dict['dim2']},'dim2')",
res[0]["py_expr"], )

def test_incomplete_expression(self):
from pysd.py_backend.vensim.vensim2py import parse_general_expression
from warnings import catch_warnings
Expand Down

0 comments on commit 2154a5f

Please sign in to comment.