|
|
@@ -27,6 +27,22 @@ class CBD2Latex:
|
|
|
render_latex (bool): When :code:`True`, the :func:`render` method will
|
|
|
output a latex-formatted string. Otherwise, simple
|
|
|
text formatting is done. Defaults to :code:`True`.
|
|
|
+ time_format (str): How the time must be formatted when rendered. By default,
|
|
|
+ it will be placed in parentheses at the end. Use
|
|
|
+ :code:`{time}` to identify the time constant.
|
|
|
+ delta_t (str): Representation of the used delta in the render. This will
|
|
|
+ be appended to values that have been computed for a delay
|
|
|
+ block. This is only to be used when the time does not
|
|
|
+ identify the iteration, but the actual simulation time.
|
|
|
+ Defaults to the empty string.
|
|
|
+ replace_par (bool): When :code:`True`, the parentheses will be replaced by the
|
|
|
+ much cleaner :code:`\\left(` and :code:`\\right)`, if rendered
|
|
|
+ in LaTeX-format. Defaults to :code:`True`.
|
|
|
+ type_formats (dict): A dictionary of :code:`{ operationType -> callable }` that
|
|
|
+ allows for the remapping of mathematical descriptions based,
|
|
|
+ where :code:`operationType` identifies the operation to remap
|
|
|
+ and :code:`callable` a function that takes the name/symbol and
|
|
|
+ the arguments as input and produces a string representation.
|
|
|
"""
|
|
|
def __init__(self, model, **kwargs):
|
|
|
self.model = model
|
|
|
@@ -35,7 +51,11 @@ class CBD2Latex:
|
|
|
"ignore_path": True,
|
|
|
"escape_nonlatex": True,
|
|
|
"time_variable": 'i',
|
|
|
- "render_latex": True
|
|
|
+ "render_latex": True,
|
|
|
+ "time_format": "({time})",
|
|
|
+ "delta_t": "",
|
|
|
+ "replace_par": True,
|
|
|
+ "type_formats": {}
|
|
|
}
|
|
|
|
|
|
for k in kwargs:
|
|
|
@@ -70,17 +90,27 @@ class CBD2Latex:
|
|
|
Technical Report <https://repository.uantwerpen.be/docman/irua/d28eb1/151279.pdf>`_
|
|
|
"""
|
|
|
# Add all blocks
|
|
|
+ TF = self.config["type_formats"]
|
|
|
for block in self.model.getBlocks():
|
|
|
func = _BLOCK_MAP.get(block.getBlockType(), None)
|
|
|
- if func is None: continue
|
|
|
+ if func is None:
|
|
|
+ func = block.getBlockType()
|
|
|
+ if func in ["OutputPortBlock", "InputPortBlock", "WireBlock"]:
|
|
|
+ continue
|
|
|
if isinstance(func, str):
|
|
|
func = lambda b, p, f=func: (p("OUT1"), Fnc(f, [p("%s") % x for x in block.getInputPortNames()]))
|
|
|
res = func(block, lambda x: self._rename(block.getPath() + "." + x))
|
|
|
if isinstance(res, tuple):
|
|
|
- self.equations[res[0]] = res[1]
|
|
|
+ f = res[1]
|
|
|
+ if isinstance(f, Fnc):
|
|
|
+ f.fmt = TF
|
|
|
+ self.equations[res[0]] = f
|
|
|
elif isinstance(res, list):
|
|
|
for r in res:
|
|
|
- self.equations[r[0]] = r[1]
|
|
|
+ f = r[1]
|
|
|
+ if isinstance(f, Fnc):
|
|
|
+ f.fmt = TF
|
|
|
+ self.equations[r[0]] = f
|
|
|
|
|
|
# Add all connections
|
|
|
for block in self.model.getBlocks():
|
|
|
@@ -106,6 +136,12 @@ class CBD2Latex:
|
|
|
if rl is None:
|
|
|
rl = self.config["render_latex"]
|
|
|
|
|
|
+ fmt = self.config["time_format"]
|
|
|
+ TF = self.config["type_formats"]
|
|
|
+ dt = self.config["delta_t"]
|
|
|
+ tvar = self.config["time_variable"]
|
|
|
+ rpar = self.config["replace_par"]
|
|
|
+
|
|
|
def apply_eq(var, val, ltx):
|
|
|
"""
|
|
|
Applies a dictionary of equations.
|
|
|
@@ -117,20 +153,22 @@ class CBD2Latex:
|
|
|
"""
|
|
|
if isinstance(val, Fnc):
|
|
|
val = deepcopy(val)
|
|
|
- val.apply_time(t=self.config["time_variable"])
|
|
|
- val = val.latex(rl)
|
|
|
+ val.apply_time(t=tvar, fmt=fmt, dt=dt)
|
|
|
+ val = val.latex(rl, tvar)
|
|
|
if len(val) > 2:
|
|
|
while val[0] == "(" and val[-1] == ")":
|
|
|
val = val[1:-1]
|
|
|
- return ltx.format(v=var, val=val, time=self.config["time_variable"])
|
|
|
+ if rl and rpar:
|
|
|
+ val = val.replace("(", "\\left(").replace(")", "\\right)")
|
|
|
+ return ltx.format(v=var, val=val, time=tvar)
|
|
|
|
|
|
for variable, value in self.equations.items():
|
|
|
- x = "\t{v}({time}) = {val}\n"
|
|
|
+ x = "\t{v}%s = {val}\n" % fmt
|
|
|
if rl:
|
|
|
- x = "\t" + r"{v}({time}) &=& {val}\\" + "\n"
|
|
|
+ x = "\t" + (r"{v}%s &=& {val}\\" % fmt) + "\n"
|
|
|
if not isinstance(value, list):
|
|
|
value = [value]
|
|
|
- latex += apply_eq(variable, Fnc('+', value), x)
|
|
|
+ latex += apply_eq(variable, Fnc('+', value, TF), x)
|
|
|
|
|
|
ic = self.create_ic()
|
|
|
for variable, value in ic.items():
|
|
|
@@ -169,9 +207,9 @@ class CBD2Latex:
|
|
|
for i in range(stop):
|
|
|
eqs = deepcopy(self.equations)
|
|
|
for k, e in eqs.items():
|
|
|
- if isinstance(e, Fnc) and e.name == 'D':
|
|
|
+ if isinstance(e, Fnc) and e.name in _MEMORY:
|
|
|
eqs[k] = Fnc('+', [e])
|
|
|
- eqs[k].apply_time(t=i)
|
|
|
+ eqs[k].apply_time(t=i, fmt=self.config["time_format"], dt=self.config["delta_t"])
|
|
|
eqs[k].apply_delay(i)
|
|
|
for c, v in created.items():
|
|
|
eqs[k].apply(c, v)
|
|
|
@@ -181,7 +219,7 @@ class CBD2Latex:
|
|
|
old = eqs[k]
|
|
|
if isinstance(eqs[k], Fnc):
|
|
|
eqs[k] = eqs[k].simplify()
|
|
|
- created["%s(%d)" % (k, i)] = eqs[k]
|
|
|
+ created["%s%s" % (k, self.config["time_format"].format(time=i))] = eqs[k]
|
|
|
return created
|
|
|
|
|
|
def simplify_links(self):
|
|
|
@@ -320,15 +358,24 @@ class Fnc:
|
|
|
name (str): The name of the function.
|
|
|
args (list): The ordered list of arguments to be applied by the
|
|
|
function.
|
|
|
+ fmt: A function that takes a name/symbol and a set of
|
|
|
+ arguments, allowing it to return a custom string
|
|
|
+ representation for the given function. When
|
|
|
+ :code:`None`, the default representation will be
|
|
|
+ used.
|
|
|
"""
|
|
|
- def __init__(self, name, args):
|
|
|
+ def __init__(self, name, args, fmt=None):
|
|
|
self.name = name
|
|
|
self.args = list(args)
|
|
|
+ self.fmt = {} if fmt is None else fmt
|
|
|
|
|
|
def __repr__(self):
|
|
|
return "%s%s" % (self.name, self.args)
|
|
|
|
|
|
def __str__(self):
|
|
|
+ f = self.fmt.get(self.name, None)
|
|
|
+ if f is not None:
|
|
|
+ return f(self.name, self.args)
|
|
|
if self.name in ['+', '*', '^', '%', '<', '<=', '==', 'or', 'and']:
|
|
|
return " {} ".format(self.name).join(["({})".format(str(x)) for x in self.args])
|
|
|
elif self.name in ['-', '!']:
|
|
|
@@ -370,7 +417,7 @@ class Fnc:
|
|
|
Args:
|
|
|
time (int): The time whence to apply the delay.
|
|
|
"""
|
|
|
- if self.name == 'D':
|
|
|
+ if self.name in _MEMORY:
|
|
|
if time == 0:
|
|
|
return self.args[1]
|
|
|
elif time > 0:
|
|
|
@@ -406,7 +453,7 @@ class Fnc:
|
|
|
if c == 1:
|
|
|
nargs.append(a)
|
|
|
else:
|
|
|
- nargs.append(Fnc("*", [a, c]))
|
|
|
+ nargs.append(Fnc("*", [a, c], self.fmt))
|
|
|
if len(nargs) == 1:
|
|
|
return nargs[0]
|
|
|
if len(nargs) == 0:
|
|
|
@@ -428,7 +475,7 @@ class Fnc:
|
|
|
if c == 1:
|
|
|
nargs.append(a)
|
|
|
else:
|
|
|
- nargs.append(Fnc("^", [a, c]))
|
|
|
+ nargs.append(Fnc("^", [a, c], self.fmt))
|
|
|
if len(nargs) == 1:
|
|
|
return nargs[0]
|
|
|
elif name == '^':
|
|
|
@@ -514,7 +561,7 @@ class Fnc:
|
|
|
if self.args[0] == self.args[1]:
|
|
|
return self.args[0]
|
|
|
|
|
|
- return Fnc(name, nargs)
|
|
|
+ return Fnc(name, nargs, self.fmt)
|
|
|
|
|
|
def is_numeric(self):
|
|
|
"""
|
|
|
@@ -540,17 +587,18 @@ class Fnc:
|
|
|
"""
|
|
|
return self.name in ["+", "-", "*", "~", "^", "root", "%", "or", "and", "==", "<=", "<"]
|
|
|
|
|
|
- def latex(self, latex=True):
|
|
|
+ def latex(self, latex=True, time_var="i"):
|
|
|
"""
|
|
|
Returns a LaTeX-formatted string of this function.
|
|
|
|
|
|
Args:
|
|
|
latex (bool): Whether or not to use LaTeX-based strings.
|
|
|
+ time_var (str): The time var string name to use for complex math.
|
|
|
"""
|
|
|
largs = deepcopy(self.args)
|
|
|
for i, a in enumerate(self.args):
|
|
|
if isinstance(a, Fnc):
|
|
|
- txt = a.latex()
|
|
|
+ txt = a.latex(latex, time_var)
|
|
|
if a.brackets():
|
|
|
largs[i] = "(%s)" % txt
|
|
|
else:
|
|
|
@@ -560,9 +608,13 @@ class Fnc:
|
|
|
else:
|
|
|
largs[i] = str(a)
|
|
|
|
|
|
+ f = self.fmt.get(self.name, None)
|
|
|
+ if f is not None:
|
|
|
+ return f(self.name, largs)
|
|
|
+
|
|
|
opers = {}
|
|
|
if latex:
|
|
|
- op = {
|
|
|
+ opers = {
|
|
|
'*': r"\cdot ",
|
|
|
'or': r"\wedge ",
|
|
|
'and': r"\vee ",
|
|
|
@@ -593,9 +645,14 @@ class Fnc:
|
|
|
return "%s %s %s" % (largs[0], op, largs[1])
|
|
|
elif self.name == 'D':
|
|
|
return largs[0]
|
|
|
+ if latex:
|
|
|
+ if self.name == 'der':
|
|
|
+ return "\\dfrac{d}{d%s} %s" % (time_var, largs[0])
|
|
|
+ if self.name == 'integral':
|
|
|
+ return "\\int_0^{%s} %s d%s" % (time_var, largs[0], time_var)
|
|
|
return "{}({})".format(self.name, ", ".join(largs))
|
|
|
|
|
|
- def apply_time(self, time=0, t="t"):
|
|
|
+ def apply_time(self, time=0, t="t", fmt="({time})", dt=""):
|
|
|
"""
|
|
|
Converts all equations to functions that take a time-argument.
|
|
|
Delay blocks decrease the "time" annotation. This function is used
|
|
|
@@ -611,13 +668,19 @@ class Fnc:
|
|
|
t (str/int): The time variable name, or an integer indicative of
|
|
|
a specific time that must be applied. E.g., setting
|
|
|
this value to 2 will apply the the formulas at time 2.
|
|
|
- """
|
|
|
- if self.name == 'D':
|
|
|
+ fmt (str): The format for the time. By default it will be placed
|
|
|
+ in parentheses at the end. Use :code:`{time}` to
|
|
|
+ identify the time variable.
|
|
|
+ dt (str): The representation of the used delta. This will be
|
|
|
+ appended after the :code:`n` value. Defaults to the
|
|
|
+ empty string.
|
|
|
+ """
|
|
|
+ if self.name in _MEMORY:
|
|
|
time += 1
|
|
|
to = t
|
|
|
if isinstance(t, str):
|
|
|
if time > 0:
|
|
|
- t += "-%d" % time
|
|
|
+ t += "-%d%s" % (time, dt)
|
|
|
if time is None:
|
|
|
t = "0"
|
|
|
else:
|
|
|
@@ -628,14 +691,14 @@ class Fnc:
|
|
|
for i, a in enumerate(self.args):
|
|
|
if isinstance(a, str):
|
|
|
if not isinstance(t, str) and t < 0:
|
|
|
- self.args[i] = "%s(0)" % a
|
|
|
+ self.args[i] = "%s%s" % (a, fmt.format(time=0))
|
|
|
else:
|
|
|
- self.args[i] = "%s(%s)" % (a, str(t))
|
|
|
+ self.args[i] = "%s%s" % (a, fmt.format(time=t))
|
|
|
elif isinstance(a, Fnc):
|
|
|
- if self.name == 'D' and i == 1:
|
|
|
- a.apply_time(None, to)
|
|
|
+ if self.name in _MEMORY and i == 1:
|
|
|
+ a.apply_time(None, to, fmt, dt)
|
|
|
else:
|
|
|
- a.apply_time(time, to)
|
|
|
+ a.apply_time(time, to, fmt, dt)
|
|
|
|
|
|
def get_delay_depth(self, start=0):
|
|
|
"""
|
|
|
@@ -645,7 +708,7 @@ class Fnc:
|
|
|
start (int): Initial count value.
|
|
|
"""
|
|
|
c = start
|
|
|
- if self.name == 'D':
|
|
|
+ if self.name in _MEMORY:
|
|
|
c += 1
|
|
|
v = [c]
|
|
|
for a in self.args:
|
|
|
@@ -689,10 +752,13 @@ _BLOCK_MAP = {
|
|
|
"OrBlock": 'or',
|
|
|
"AndBlock": 'and',
|
|
|
"DelayBlock": 'D',
|
|
|
- # "DelayBlock": lambda block, p: [(p(.UT1"), Fnc('D', [p(.N1")])), (p(.UT1()0)", Fnc('S', [p(.C")]))],
|
|
|
- "TimeBlock": lambda block, p: (p("OUT1"), 't')
|
|
|
+ "TimeBlock": lambda block, p: (p("OUT1"), 't'),
|
|
|
+ "DerivatorBlock": 'der',
|
|
|
+ "IntegratorBlock": 'integral',
|
|
|
}
|
|
|
|
|
|
+_MEMORY = ['D', 'der', 'integral']
|
|
|
+
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
from CBD.Core import CBD
|
|
|
@@ -717,7 +783,7 @@ if __name__ == '__main__':
|
|
|
# Create the Blocks
|
|
|
self.addBlock(DelayBlock("delay1"))
|
|
|
self.addBlock(DelayBlock("delay2"))
|
|
|
- self.addBlock(DelayBlock("delay3"))
|
|
|
+ self.addBlock(DerivatorBlock("delay3"))
|
|
|
self.addBlock(AdderBlock("sum"))
|
|
|
self.addBlock(ConstantBlock("zero", value=(0)))
|
|
|
self.addBlock(ConstantBlock("one", value=(1)))
|
|
|
@@ -738,6 +804,7 @@ if __name__ == '__main__':
|
|
|
ltx = CBD2Latex(FibonacciGen("fib"), render_latex=False, show_steps=True)
|
|
|
# ltx.render()
|
|
|
ltx.simplify()
|
|
|
+ print("----------------------------")
|
|
|
print(ltx.render())
|
|
|
print("----------------------------")
|
|
|
print(ltx.eq())
|