diff --git a/pysd/__init__.py b/pysd/__init__.py index 4666c714..545eb310 100644 --- a/pysd/__init__.py +++ b/pysd/__init__.py @@ -1,4 +1,5 @@ from .pysd import read_vensim, read_xmile, load +from . import py_backend from .py_backend import functions from .py_backend import utils from ._version import __version__ diff --git a/pysd/py_backend/functions.py b/pysd/py_backend/functions.py index e62acf57..bef4ad59 100644 --- a/pysd/py_backend/functions.py +++ b/pysd/py_backend/functions.py @@ -747,9 +747,9 @@ def _integrate(self, time_steps, capture_elements, return_timestamps): return outputs -def ramp(slope, start, finish): +def ramp(slope, start, finish=0): """ - Implements vensim's RAMP function + Implements vensim's and xmile's RAMP function Parameters ---------- @@ -758,7 +758,7 @@ def ramp(slope, start, finish): start: float Time at which the ramp begins finish: float - Time at which the ramo ends + Optional. Time at which the ramp ends Returns ------- @@ -773,10 +773,13 @@ def ramp(slope, start, finish): t = time() if t < start: return 0 - elif t > finish: - return slope * (finish - start) else: - return slope * (t - start) + if finish <= 0: + return slope * (t - start) + elif t > finish: + return slope * (finish - start) + else: + return slope * (t - start) def step(value, tstep): @@ -808,7 +811,6 @@ def pulse(start, duration): t = time() return 1 if start <= t < start + duration else 0 - def pulse_train(start, duration, repeat_time, end): """ Implements vensim's PULSE TRAIN function @@ -822,6 +824,31 @@ def pulse_train(start, duration, repeat_time, end): else: return 0 +def pulse_magnitude(magnitude, start, repeat_time=0): + """ Implements xmile's PULSE function + + PULSE: Generate a one-DT wide pulse at the given time + Parameters: 2 or 3: (magnitude, first time[, interval]) + Without interval or when interval = 0, the PULSE is generated only once + Example: PULSE(20, 12, 5) generates a pulse value of 20/DT at time 12, 17, 22, etc. + + In rage [-inf, start) returns 0 + In range [start + n * repeat_time, start + n * repeat_time + dt) return magnitude/dt + In rage [start + n * repeat_time + dt, start + (n + 1) * repeat_time) return 0 + """ + t = time() + small = 1e-6 # What is considered zero according to Vensim Help + if repeat_time <= small: + if abs(t - start) < time_step: + return magnitude * time_step + else: + return 0 + else: + if abs((t - start) % repeat_time) < time_step: + return magnitude * time_step + else: + return 0 + def lookup(x, xs, ys): """ Provides the working mechanism for lookup functions the builder builds """ diff --git a/pysd/py_backend/xmile/SMILE2Py.py b/pysd/py_backend/xmile/SMILE2Py.py index 0d179d04..dde7cc18 100644 --- a/pysd/py_backend/xmile/SMILE2Py.py +++ b/pysd/py_backend/xmile/SMILE2Py.py @@ -23,26 +23,106 @@ # Here we define which python function each XMILE keyword corresponds to functions = { - "abs": "abs", "int": "int", "exp": "np.exp", "inf": "np.inf", "log10": "np.log10", - "pi": "np.pi", "sin": "np.sin", "cos": "np.cos", "sqrt": "np.sqrt", "tan": "np.tan", - "lognormal": "np.random.lognormal", "normal": "np.random.normal", - "poisson": "np.random.poisson", "ln": "np.log", "exprnd": "np.random.exponential", - "random": "np.random.rand", "min": "min", "max": "max", "arccos": "np.arccos", - "arcsin": "np.arcsin", "arctan": "np.arctan", + # === + # 3.5.1 Mathematical Functions + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039980 + # === + + "abs": "abs", + "arccos": "np.arccos", + "arcsin": "np.arcsin", + "arctan": "np.arctan", + "cos": "np.cos", + "sin": "np.sin", + "tan": "np.tan", + "exp": "np.exp", + "inf": "np.inf", + "int": "int", + "ln": "np.log", + "log10": "np.log10", + "max": "max", + "min": "min", + "pi": "np.pi", + "sqrt": "np.sqrt", + + # === + # 3.5.2 Statistical Functions + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039981 + # === + + "exprnd": "np.random.exponential", + "lognormal": "np.random.lognormal", + "normal": "np.random.normal", + "poisson": "np.random.poisson", + "random": "np.random.rand", + + # ==== + # 3.5.3 Delay Functions + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039982 + # ==== + + # "delay" !TODO! + # "delay1" !TODO! + # "delay2" !TODO! + # "delay3" !TODO! + # "delayn" !TODO! + # "forcst" !TODO! + # "smth1" !TODO! + # "smth3" !TODO! + # "smthn" !TODO! + # "trend" !TODO! + + # === + # 3.5.4 Test Input Functions + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039983 + # === + + "pulse": "functions.pulse_magnitude", + "step": "functions.step", + "ramp": "functions.ramp", + + # === + # 3.5.5 Time Functions + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039984 + # === + # Should we include as function list or it provided by another way? + + # "dt" !TODO! + # "starttime" !TODO! + # "stoptime" !TODO! + # "time" !TODO! + + # === + # 3.5.6 Miscellaneous Functions + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039985 + # === "if_then_else": "functions.if_then_else", - "step": "functions.step", "pulse": "functions.pulse" + # "init" !TODO! + # "previous" !TODO! + # "self" !TODO! } prefix_operators = { "not": " not ", - "-": "-", "+": " ", + "-": "-", + "+": " ", } infix_operators = { - "and": " and ", "or": " or ", - "=": "==", "<=": "<=", "<": "<", ">=": ">=", ">": ">", "<>": "!=", - "^": "**", "+": "+", "-": "-", - "*": "*", "/": "/", "mod": "%", + "and": " and ", + "or": " or ", + "=": "==", + "<=": "<=", + "<": "<", + ">=": ">=", + ">": ">", + "<>": "!=", + "^": "**", + "+": "+", + "-": "-", + "*": "*", + "/": "/", + "mod": "%", } builders = {} @@ -79,10 +159,16 @@ def parse(self, text, context='eqn'): If context is set to equation, lone identifiers will be parsed as calls to elements If context is set to definition, lone identifiers will be cleaned and returned. """ + # !TODO! Should remove the inline comments from `text` before parsing the grammar + # http://docs.oasis-open.org/xmile/xmile/v1.0/csprd01/xmile-v1.0-csprd01.html#_Toc398039973 self.ast = self.grammar.parse(text) self.context = context return self.visit(self.ast) + def visit_conditional_statement(self, n, vc): + _IF, _1, condition_expr, _2, _THEN, _3, then_expr, _4, _ELSE, _5, else_expr = vc + return "functions.if_then_else(" + condition_expr + ", " + then_expr + ", " + else_expr + ")" + def visit_identifier(self, n, vc): return self.extended_model_namespace[n.text] + '()' diff --git a/pysd/py_backend/xmile/smile.grammar b/pysd/py_backend/xmile/smile.grammar index 027c7a4e..119e00cf 100644 --- a/pysd/py_backend/xmile/smile.grammar +++ b/pysd/py_backend/xmile/smile.grammar @@ -1,4 +1,6 @@ -expr = _ pre_oper? _ primary _ (in_oper _ expr)? +expr = conditional_statement / (_ pre_oper? _ primary _ (in_oper _ expr)?) + +conditional_statement = "IF" _ expr _ "THEN" _ expr _ "ELSE" _ expr primary = call / parens / number / identifier parens = "(" _ expr _ ")" diff --git a/pysd/pysd.py b/pysd/pysd.py index 22d4852b..4882a855 100644 --- a/pysd/pysd.py +++ b/pysd/pysd.py @@ -15,6 +15,7 @@ def read_xmile(xmile_file): """ Construct a model object from `.xmile` file. """ + from . import py_backend from .py_backend.xmile.xmile2py import translate_xmile py_model_file = translate_xmile(xmile_file) model = load(py_model_file)