Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add random functions tests and truncexpon #441

Merged
merged 1 commit into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()),
{}
)
Loading