Skip to content

Commit

Permalink
Add support for GET TIME VALUE
Browse files Browse the repository at this point in the history
  • Loading branch information
enekomartinmartinez committed Jun 10, 2022
1 parent 08156b5 commit dda167f
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 10 deletions.
1 change: 0 additions & 1 deletion docs/structure/vensim_translation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,5 @@ Vensim macros are supported. The macro content between the keywords \:MACRO: and
Planed New Functions and Features
---------------------------------
- ALLOCATE BY PRIORITY
- GET TIME VALUE
- SHIFT IF TRUE
- VECTOR SELECT
1 change: 1 addition & 0 deletions docs/tables/functions.tab
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ PULSE TRAIN PULSE TRAIN(start, width, tbetween, end) "CallStructure('pulse_tra
RAMP RAMP(slope, start_time, end_time) ramp ramp(slope, start_time, end_time) "CallStructure('ramp', (slope, start_time, end_time))" pysd.functions.ramp(time, slope, start_time, end_time) Not tested for Xmile!
ramp ramp(slope, start_time) "CallStructure('ramp', (slope, start_time))" pysd.functions.ramp(time, slope, start_time) Not tested for Xmile!
STEP STEP(height, step_time) step step(height, step_time) "CallStructure('step', (height, step_time))" pysd.functions.step(time, height, step_time) Not tested for Xmile!
GET TIME VALUE GET TIME VALUE(relativeto, offset, measure) "CallStructure('get_time_value', (relativeto, offset, measure))" pysd.functions.get_time_value(time, relativeto, offset, measure) Not all the cases implemented!
VECTOR RANK VECTOR RANK(vec, direction) "CallStructure('vector_rank', (vec, direction))" vector_rank(vec, direction)
VECTOR REORDER VECTOR REORDER(vec, svec) "CallStructure('vector_reorder', (vec, svec))" vector_reorder(vec, svec)
VECTOR SORT ORDER VECTOR SORT ORDER(vec, direction) "CallStructure('vector_sort_order', (vec, direction))" vector_sort_order(vec, direction)
Expand Down
28 changes: 28 additions & 0 deletions docs/whats_new.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@

What's New
==========

v3.2.0 (unreleased)
-------------------

New Features
~~~~~~~~~~~~
- Add support for Vensim's `GET TIME VALUE <https://www.vensim.com/documentation/fn_get_time_value.html>`_ (:func:`pysd.py_backend.functions.get_time_value`) function (:issue:`332`). Not all cases have been implemented.

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

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

Bug fixes
~~~~~~~~~

Documentation
~~~~~~~~~~~~~

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

Internal Changes
~~~~~~~~~~~~~~~~



v3.1.0 (2022/06/02)
-------------------

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.1.0"
__version__ = "3.2.0"
3 changes: 3 additions & 0 deletions pysd/builders/python/python_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
"Xpulse_train": (
"pulse(__data['time'], %(0)s, repeat_time=%(1)s, magnitude=%(2)s)",
("functions", "pulse")),
"get_time_value": (
"get_time_value(__data['time'], %(0)s, %(1)s, %(2)s)",
("functions", "get_time_value")),
"quantum": (
"quantum(%(0)s, %(1)s)",
("functions", "quantum")),
Expand Down
130 changes: 124 additions & 6 deletions pysd/py_backend/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Stella, but sometimes the number or order of arguments may change.
"""
import warnings
from datetime import datetime

import numpy as np
import xarray as xr
Expand All @@ -22,8 +23,8 @@ def ramp(time, slope, start, finish=None):
Parameters
----------
time: callable
Function that returns the current time.
time: pysd.py_backend.components.Time
Model time object.
slope: float
The slope of the ramp starting at zero at time start.
start: float
Expand Down Expand Up @@ -57,8 +58,8 @@ def step(time, value, tstep):
Parameters
----------
time: callable
Function that returns the current time.
time: pysd.py_backend.components.Time
Model time object.
value: float
The height of the step.
tstep: float
Expand All @@ -82,8 +83,8 @@ def pulse(time, start, repeat_time=0, width=None, magnitude=None, end=None):
Parameters
----------
time: callable
Function that returns the current time.
time: pysd.py_backend.components.Time
Model time object.
start: float
Starting time of the pulse.
repeat_time: float (optional)
Expand Down Expand Up @@ -577,3 +578,120 @@ def vector_rank(vector, direction):
"""
return vector_sort_order(vector, direction).argsort() + 1


def get_time_value(time, relativeto, offset, measure):
"""
Implements Vensim's GET TIME VALUE function. Warning, not all the
cases are implemented.
https://www.vensim.com/documentation/fn_get_time_value.html
Parameters
----------
time: pysd.py_backend.components.Time
Model time object.
relativeto: int
The time to take as a reference:
- 0 for the current simulation time.
- 1 for the initial simulation time.
- 2 for the current computer clock time.
offset: float or xarray.DataArray
The difference in time, as measured in the units of Time for
the model, to move before computing the value. offset is
ignored when relativeto is 2.
measure: int
The units or measure of time:
- 0 units of Time in the model (only for relativeto 0 and 1)
- 1 years since 1 BC (an integer, same as the normal calendar year)
- 2 quarter of year (1-4)
- 3 month of year (1-12)
- 4 day of month (1-31)
- 5 day of week (0-6 where 0 is Sunday)
- 6 days since Jan 1., 1 BC (year 1 BC is treated as year 0)
- 7 hour of day (0-23)
- 8 minute of hour (0-59)
- 9 second of minute (0-59.999999 – not an integer)
- 10 elapsed seconds modulo 500,000 (0-499,999)
Returns
-------
time_value: float or int
The resulting time value.
"""
if relativeto == 0:
# Current time
ctime = time()
elif relativeto == 1:
# Initial time
# Not implemented as it doesn't work as Vensim docs say
# TODO check other versions or implement it as it should be?
raise NotImplementedError("'relativeto=1' not implemented...")
# ctime = time.initial_time()
elif relativeto == 2:
# Machine time
ctime = utils.get_current_computer_time()
else:
# Invalid value
raise ValueError(
f"Invalid argument value 'relativeto={relativeto}'. "
"'relativeto' must be 0, 1 or 2.")

if measure == 0:
# units of Time in the model (only for relativeto 0 and 1)
if relativeto == 2:
# measure=0 only supported with relativeto=0 or 1
raise ValueError(
"Invalid argument 'measure=0' with 'relativeto=2'.")
else:
return ctime + offset
elif measure == 1:
# years since 1 BC (an integer, same as the normal calendar year)
if relativeto == 2:
return ctime.year
elif measure == 2:
# quarter of year (1-4)
if relativeto == 2:
return int(1 + (ctime.month-0.5) // 3)
elif measure == 3:
# month of year (1-12)
if relativeto == 2:
return ctime.month
elif measure == 4:
# day of month (1-31)
if relativeto == 2:
return ctime.day
elif measure == 5:
# day of week (0-6 where 0 is Sunday)
if relativeto == 2:
return ctime.weekday()
elif measure == 6:
# days since Jan 1., 1 BC (year 1 BC is treated as year 0)
if relativeto == 2:
return (ctime - datetime(1, 1, 1)).days
elif measure == 7:
# hour of day (0-23)
if relativeto == 2:
return ctime.hour
elif measure == 8:
# minute of hour (0-59)
if relativeto == 2:
return ctime.minute
elif measure == 9:
# second of minute (0-59.99 – not an integer)
if relativeto == 2:
return ctime.second + 1e-6*ctime.microsecond
elif measure == 10:
# elapsed seconds modulo 500,000 (0-499,999)
if relativeto == 2:
return (ctime - datetime(1, 1, 1)).seconds % 500000
else:
raise ValueError(
f"Invalid argument value 'measure={measure}'. "
"'measure' must be 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 or 10.")

# TODO include other measures for relativeto=0
raise NotImplementedError(
f"The case 'relativeto={relativeto}' and 'measure={measure}' "
"is not implemented..."
)
19 changes: 19 additions & 0 deletions pysd/py_backend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import json
from datetime import datetime
from pathlib import Path
from chardet.universaldetector import UniversalDetector

Expand Down Expand Up @@ -36,6 +37,24 @@ def xrsplit(array):
return sp_list


def get_current_computer_time():
"""
Returns the current machine time. Needed to mock the machine time in
the tests.
Parameters
---------
None
Returns
-------
datetime.now(): datetime.datetime
Current machine time.
"""
return datetime.now()


def get_return_elements(return_columns, namespace):
"""
Takes a list of return elements formatted in vensim's format
Expand Down
4 changes: 4 additions & 0 deletions tests/pytest_integration/pytest_integration_vensim_pathway.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@
"folder": "get_subscript_3d_arrays_xls",
"file": "test_get_subscript_3d_arrays_xls.mdl"
},
"get_time_value": {
"folder": "get_time_value",
"file": "test_get_time_value_simple.mdl"
},
"get_with_missing_values_xlsx": {
"folder": "get_with_missing_values_xlsx",
"file": "test_get_with_missing_values_xlsx.mdl"
Expand Down
107 changes: 106 additions & 1 deletion tests/pytest_pysd/pytest_functions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from datetime import datetime

import pytest
import numpy as np
import xarray as xr

from pysd.py_backend.components import Time
from pysd.py_backend.functions import\
ramp, step, pulse, xidz, zidz, if_then_else, sum, prod, vmin, vmax,\
invert_matrix
invert_matrix, get_time_value


class TestInputFunctions():
Expand Down Expand Up @@ -334,6 +336,109 @@ def test_invert_matrix(self):
< 1e-14
).all()

@pytest.mark.parametrize(
"year,quarter,month,day,hour,minute,second,microsecond",
[
(2020, 1, 1, 25, 23, 45, 5, 233),
(2020, 1, 3, 4, 15, 10, 23, 3323),
(195, 2, 4, 4, 15, 10, 23, 3323),
(195, 2, 6, 4, 15, 10, 23, 3323),
(2045, 3, 7, 31, 0, 15, 55, 33233),
(1330, 3, 9, 30, 0, 15, 55, 33233),
(3000, 4, 10, 1, 13, 15, 55, 33233),
(1995, 4, 12, 1, 13, 15, 55, 33233),
]
)
def test_get_time_value_machine(self, mocker, year, quarter, month, day,
hour, minute, second, microsecond):
"""Test get_time_value with machine time reltiveto=2"""

mock_time = datetime(
year, month, day, hour, minute, second, microsecond)

mocker.patch(
"pysd.py_backend.utils.get_current_computer_time",
return_value=mock_time)

assert get_time_value(None, 2, np.random.randint(-100, 100), 1)\
== year

assert get_time_value(None, 2, np.random.randint(-100, 100), 2)\
== quarter

assert get_time_value(None, 2, np.random.randint(-100, 100), 3)\
== month

assert get_time_value(None, 2, np.random.randint(-100, 100), 4)\
== day

assert get_time_value(None, 2, np.random.randint(-100, 100), 5)\
== mock_time.weekday()

assert get_time_value(None, 2, np.random.randint(-100, 100), 6)\
== (mock_time - datetime(1, 1, 1)).days

assert get_time_value(None, 2, np.random.randint(-100, 100), 7)\
== hour

assert get_time_value(None, 2, np.random.randint(-100, 100), 8)\
== minute

assert get_time_value(None, 2, np.random.randint(-100, 100), 9)\
== second + 1e-6*microsecond

assert get_time_value(None, 2, np.random.randint(-100, 100), 10)\
== (mock_time - datetime(1, 1, 1)).seconds % 500000

@pytest.mark.parametrize(
"measure,relativeto,raise_type,error_message",
[
( # relativeto=1
0,
1,
NotImplementedError,
r"'relativeto=1' not implemented\.\.\."
),
( # relativeto=3
0,
3,
ValueError,
r"Invalid argument value 'relativeto=3'\. "
r"'relativeto' must be 0, 1 or 2\."
),
( # measure=0;relativeto=2
0,
2,
ValueError,
r"Invalid argument 'measure=0' with 'relativeto=2'\."
),
( # measure=11
11,
2,
ValueError,
r"Invalid argument value 'measure=11'\. "
r"'measure' must be 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 or 10\."
),
( # relativeto=0;measure=2
2,
0,
NotImplementedError,
r"The case 'relativeto=0' and 'measure=2' "
r"is not implemented\.\.\."
),
],
ids=[
"relativeto=1", "relativeto=3", "measure=0;relativeto=2",
"measure=11", "relativeto=0;measure=2"
]
)
def test_get_time_value_errors(self, measure, relativeto,
raise_type, error_message):

with pytest.raises(raise_type, match=error_message):
get_time_value(
lambda: 0, relativeto, np.random.randint(-100, 100), measure)

def test_incomplete(self):
from pysd.py_backend.functions import incomplete

Expand Down
Loading

0 comments on commit dda167f

Please sign in to comment.