Skip to content

Commit

Permalink
Merge pull request SDXorg#144 from alexprey/structuring_readers
Browse files Browse the repository at this point in the history
Improve supporting for XMILE specification
  • Loading branch information
JamesPHoughton authored Sep 29, 2017
2 parents 1b343cf + 3cf1765 commit 91ce868
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 20 deletions.
1 change: 1 addition & 0 deletions pysd/__init__.py
Original file line number Diff line number Diff line change
@@ -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__
Expand Down
41 changes: 34 additions & 7 deletions pysd/py_backend/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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
-------
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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 """
Expand Down
110 changes: 98 additions & 12 deletions pysd/py_backend/xmile/SMILE2Py.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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] + '()'

Expand Down
4 changes: 3 additions & 1 deletion pysd/py_backend/xmile/smile.grammar
Original file line number Diff line number Diff line change
@@ -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 _ ")"
Expand Down
1 change: 1 addition & 0 deletions pysd/pysd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 91ce868

Please sign in to comment.