Skip to content

Commit

Permalink
Add random functions tests and truncexpon
Browse files Browse the repository at this point in the history
  • Loading branch information
enekomartinmartinez committed Apr 24, 2024
1 parent a6064b7 commit 04fafd1
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 62 deletions.
4 changes: 4 additions & 0 deletions docs/tables/functions.tab
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ ALLOCATE AVAILABLE "ALLOCATE AVAILABLE(request, pp, avail)" "AllocateAvailable
ALLOCATE BY PRIORITY "ALLOCATE BY PRIORITY(request, priority, size, width, supply)" "AllocateByPriorityStructure(request, priority, size, width, supply)" allocate_by_priority(request, priority, width, supply)
INITIAL INITIAL(value) init init(value) InitialStructure(value) pysd.statefuls.Initial
SAMPLE IF TRUE "SAMPLE IF TRUE(condition, input, initial_value)" "SampleIfTrueStructure(condition, input, initial_value)" pysd.statefuls.SampleIfTrue(...)
RANDOM 0 1 "RANDOM 0 1()" "CallStructure('random_0_1', ())" np.random.uniform(0, 1, size=final_shape)
RANDOM UNIFORM "RANDOM UNIFORM(m, x, s)" "CallStructure('random_uniform', (m, x, s))" np.random.uniform(m, x, size=final_shape)
RANDOM NORMAL "RANDOM NORMAL(m, x, h, r, s)" "CallStructure('random_normal', (m, x, h, r, s))" stats.truncnorm.rvs((m-h)/r, (x-h)/r, loc=h, scale=r, size=final_shape)
RANDOM EXPONENTIAL "RANDOM EXPONENTIAL(m, x, h, r, s)" "CallStructure('random_exponential', (m, x, h, r, s))" stats.truncexpon.rvs((x-np.maximum(m, h))/r, loc=np.maximum(m, h), scale=r, size=final_shape)
28 changes: 28 additions & 0 deletions docs/whats_new.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
What's New
==========
v3.14.0 (2024/04/24)
--------------------
New Features
~~~~~~~~~~~~
- Support Vensim's `RANDOM EXPONENTIAL <https://www.vensim.com/documentation/fn_random.html>`_ function (:issue:`107`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

Breaking changes
~~~~~~~~~~~~~~~~

Deprecations
~~~~~~~~~~~~

Bug fixes
~~~~~~~~~
- Fix truncation in Vensim's `RANDOM NORMAL <https://www.vensim.com/documentation/fn_random.html>`_ function translation. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

Documentation
~~~~~~~~~~~~~
- Add supported random functions to the documentation tables. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

Performance
~~~~~~~~~~~

Internal Changes
~~~~~~~~~~~~~~~~
- Add test for random functions inlcuying comparison with Vensim outputs and expected values (:issue:`107`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)
- Allow to add multiple imports by the python function call builder. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

v3.13.4 (2024/02/29)
--------------------
New Features
Expand Down
2 changes: 1 addition & 1 deletion pysd/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.13.4"
__version__ = "3.14.0"
4 changes: 2 additions & 2 deletions pysd/builders/python/python_expressions_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,9 +606,9 @@ def build_function_call(self, arguments: dict) -> BuildAST:
"""
# Get the function expression from the functionspace
expression, modules = functionspace[self.function]
if modules:
for module in modules:
# Update module dependencies in imports
self.section.imports.add(*modules)
self.section.imports.add(*module)

calls = self.join_calls(arguments)

Expand Down
116 changes: 59 additions & 57 deletions pysd/builders/python/python_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,118 +2,120 @@
# functions that can be diretcly applied over an array
functionspace = {
# directly build functions without dependencies
"elmcount": ("len(%(0)s)", None),
"elmcount": ("len(%(0)s)", ()),

# directly build numpy based functions
"pi": ("np.pi", ("numpy",)),
"abs": ("np.abs(%(0)s)", ("numpy",)),
"power": ("np.power(%(0)s,%(1)s)", ("numpy",)),
"min": ("np.minimum(%(0)s, %(1)s)", ("numpy",)),
"max": ("np.maximum(%(0)s, %(1)s)", ("numpy",)),
"exp": ("np.exp(%(0)s)", ("numpy",)),
"sin": ("np.sin(%(0)s)", ("numpy",)),
"cos": ("np.cos(%(0)s)", ("numpy",)),
"tan": ("np.tan(%(0)s)", ("numpy",)),
"arcsin": ("np.arcsin(%(0)s)", ("numpy",)),
"arccos": ("np.arccos(%(0)s)", ("numpy",)),
"arctan": ("np.arctan(%(0)s)", ("numpy",)),
"sinh": ("np.sinh(%(0)s)", ("numpy",)),
"cosh": ("np.cosh(%(0)s)", ("numpy",)),
"tanh": ("np.tanh(%(0)s)", ("numpy",)),
"sqrt": ("np.sqrt(%(0)s)", ("numpy",)),
"ln": ("np.log(%(0)s)", ("numpy",)),
"log": ("(np.log(%(0)s)/np.log(%(1)s))", ("numpy",)),
# NUMPY: "invert_matrix": ("np.linalg.inv(%(0)s)", ("numpy",)),
"pi": ("np.pi", (("numpy",),)),
"abs": ("np.abs(%(0)s)", (("numpy",),)),
"power": ("np.power(%(0)s,%(1)s)", (("numpy",),)),
"min": ("np.minimum(%(0)s, %(1)s)", (("numpy",),)),
"max": ("np.maximum(%(0)s, %(1)s)", (("numpy",),)),
"exp": ("np.exp(%(0)s)", (("numpy",),)),
"sin": ("np.sin(%(0)s)", (("numpy",),)),
"cos": ("np.cos(%(0)s)", (("numpy",),)),
"tan": ("np.tan(%(0)s)", (("numpy",),)),
"arcsin": ("np.arcsin(%(0)s)", (("numpy",),)),
"arccos": ("np.arccos(%(0)s)", (("numpy",),)),
"arctan": ("np.arctan(%(0)s)", (("numpy",),)),
"sinh": ("np.sinh(%(0)s)", (("numpy",),)),
"cosh": ("np.cosh(%(0)s)", (("numpy",),)),
"tanh": ("np.tanh(%(0)s)", (("numpy",),)),
"sqrt": ("np.sqrt(%(0)s)", (("numpy",),)),
"ln": ("np.log(%(0)s)", (("numpy",),)),
"log": ("(np.log(%(0)s)/np.log(%(1)s))", (("numpy",),)),
# NUMPY: "invert_matrix": ("np.linalg.inv(%(0)s)", (("numpy",),)),

# vector functions with axis to apply over
# NUMPY:
# "prod": "np.prod(%(0)s, axis=%(axis)s)", ("numpy",)),
# "sum": "np.sum(%(0)s, axis=%(axis)s)", ("numpy",)),
# "vmax": "np.max(%(0)s, axis=%(axis)s)", ("numpy", )),
# "vmin": "np.min(%(0)s, axis=%(axis)s)", ("numpy",))
"prod": ("prod(%(0)s, dim=%(axis)s)", ("functions", "prod")),
"sum": ("sum(%(0)s, dim=%(axis)s)", ("functions", "sum")),
"vmax": ("vmax(%(0)s, dim=%(axis)s)", ("functions", "vmax")),
"vmin": ("vmin(%(0)s, dim=%(axis)s)", ("functions", "vmin")),
"vmax_xmile": ("vmax(%(0)s)", ("functions", "vmax")),
"vmin_xmile": ("vmin(%(0)s)", ("functions", "vmin")),
# "prod": "np.prod(%(0)s, axis=%(axis)s)", (("numpy",),)),
# "sum": "np.sum(%(0)s, axis=%(axis)s)", (("numpy",),)),
# "vmax": "np.max(%(0)s, axis=%(axis)s)", ("numpy",),)),
# "vmin": "np.min(%(0)s, axis=%(axis)s)", (("numpy",),))
"prod": ("prod(%(0)s, dim=%(axis)s)", (("functions", "prod"),)),
"sum": ("sum(%(0)s, dim=%(axis)s)", (("functions", "sum"),)),
"vmax": ("vmax(%(0)s, dim=%(axis)s)", (("functions", "vmax"),)),
"vmin": ("vmin(%(0)s, dim=%(axis)s)", (("functions", "vmin"),)),
"vmax_xmile": ("vmax(%(0)s)", (("functions", "vmax"),)),
"vmin_xmile": ("vmin(%(0)s)", (("functions", "vmin"),)),
"vector_select": (
"vector_select(%(0)s, %(1)s, %(axis)s, %(2)s, %(3)s, %(4)s)",
("functions", "vector_select")
(("functions", "vector_select"),)
),

# functions defined in pysd.py_bakcend.functions
"active_initial": (
"active_initial(__data[\"time\"].stage, lambda: %(0)s, %(1)s)",
("functions", "active_initial")),
(("functions", "active_initial"),)),
"if_then_else": (
"if_then_else(%(0)s, lambda: %(1)s, lambda: %(2)s)",
("functions", "if_then_else")),
(("functions", "if_then_else"),)),
"integer": (
"integer(%(0)s)",
("functions", "integer")),
(("functions", "integer"),)),
"invert_matrix": ( # NUMPY: remove
"invert_matrix(%(0)s)",
("functions", "invert_matrix")), # NUMPY: remove
(("functions", "invert_matrix"),)), # NUMPY: remove
"modulo": (
"modulo(%(0)s, %(1)s)",
("functions", "modulo")),
(("functions", "modulo"),)),
"pulse": (
"pulse(__data['time'], %(0)s, width=%(1)s)",
("functions", "pulse")),
(("functions", "pulse"),)),
"Xpulse": (
"pulse(__data['time'], %(0)s, magnitude=%(1)s)",
("functions", "pulse")),
(("functions", "pulse"),)),
"pulse_train": (
"pulse(__data['time'], %(0)s, repeat_time=%(1)s, width=%(2)s, "\
"end=%(3)s)",
("functions", "pulse")),
(("functions", "pulse"),)),
"Xpulse_train": (
"pulse(__data['time'], %(0)s, repeat_time=%(1)s, magnitude=%(2)s)",
("functions", "pulse")),
(("functions", "pulse"),)),
"get_time_value": (
"get_time_value(__data['time'], %(0)s, %(1)s, %(2)s)",
("functions", "get_time_value")),
(("functions", "get_time_value"),)),
"quantum": (
"quantum(%(0)s, %(1)s)",
("functions", "quantum")),
(("functions", "quantum"),)),
"Xramp": (
"ramp(__data['time'], %(0)s, %(1)s)",
("functions", "ramp")),
(("functions", "ramp"),)),
"ramp": (
"ramp(__data['time'], %(0)s, %(1)s, %(2)s)",
("functions", "ramp")),
(("functions", "ramp"),)),
"step": (
"step(__data['time'], %(0)s, %(1)s)",
("functions", "step")),
(("functions", "step"),)),
"xidz": (
"xidz(%(0)s, %(1)s, %(2)s)",
("functions", "xidz")),
(("functions", "xidz"),)),
"zidz": (
"zidz(%(0)s, %(1)s)",
("functions", "zidz")),
(("functions", "zidz"),)),
"vector_sort_order": (
"vector_sort_order(%(0)s, %(1)s)",
("functions", "vector_sort_order")),
(("functions", "vector_sort_order"),)),
"vector_reorder": (
"vector_reorder(%(0)s, %(1)s)",
("functions", "vector_reorder")),
(("functions", "vector_reorder"),)),
"vector_rank": (
"vector_rank(%(0)s, %(1)s)",
("functions", "vector_rank")),
(("functions", "vector_rank"),)),

# random functions must have the shape of the component subscripts
# most of them are shifted, scaled and truncated
# TODO: it is difficult to find same parametrization in Python,
# maybe build a new model
"random_0_1": (
"np.random.uniform(0, 1, size=%(size)s)",
("numpy",)),
(("numpy",),)),
"random_uniform": (
"np.random.uniform(%(0)s, %(1)s, size=%(size)s)",
("numpy",)),
(("numpy",),)),
"random_normal": (
"stats.truncnorm.rvs(%(0)s, %(1)s, loc=%(2)s, scale=%(3)s,"
" size=%(size)s)",
("scipy", "stats")),
"stats.truncnorm.rvs((%(0)s-%(2)s)/%(3)s, (%(1)s-%(2)s)/%(3)s,"
" loc=%(2)s, scale=%(3)s, size=%(size)s)",
(("scipy", "stats"),)),
"random_exponential": (
"stats.truncexpon.rvs((%(1)s-np.maximum(%(0)s, %(2)s))/%(3)s,"
" loc=np.maximum(%(0)s, %(2)s), scale=%(3)s, size=%(size)s)",
(("scipy", "stats"), ("numpy",),)),
}
46 changes: 46 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import shutil
from pathlib import Path
from dataclasses import dataclass


import pytest

from pysd import read_vensim, read_xmile, load
from pysd.translators.vensim.vensim_utils import supported_extensions as\
vensim_extensions
from pysd.translators.xmile.xmile_utils import supported_extensions as\
xmile_extensions

from pysd.builders.python.imports import ImportsManager


@pytest.fixture(scope="session")
def _root():
Expand All @@ -21,6 +26,12 @@ def _test_models(_root):
return _root.joinpath("test-models/tests")


@pytest.fixture(scope="session")
def _test_random(_root):
# test-models directory
return _root.joinpath("test-models/random")


@pytest.fixture(scope="class")
def shared_tmpdir(tmp_path_factory):
# shared temporary directory for each class
Expand Down Expand Up @@ -58,3 +69,38 @@ def ignore_warns():
"future version. Use timezone-aware objects to represent datetimes "
"in UTC.*",
]


@pytest.fixture(scope="session")
def random_size():
# size of generated random samples
return int(1e6)


@dataclass
class FakeComponent:
element: str
section: object
subscripts_dict: dict


@dataclass
class FakeSection:
namespace: object
macrospace: dict
imports: object


@dataclass
class FakeNamespace:
cleanspace: dict


@pytest.fixture(scope="function")
def fake_component():
# fake_component used to translate random functions to python
return FakeComponent(
'',
FakeSection(FakeNamespace({}), {}, ImportsManager()),
{}
)

0 comments on commit 04fafd1

Please sign in to comment.