Browse Source

Added more features to latexify module

rparedis 4 years ago
parent
commit
14aa42353b
4 changed files with 121 additions and 51 deletions
  1. 2 0
      doc/changelog.rst
  2. 17 16
      examples/HarmonicOscilator/CBDA.py
  3. 101 34
      src/CBD/converters/latexify.py
  4. 1 1
      src/CBD/lib/std.py

+ 2 - 0
doc/changelog.rst

@@ -4,6 +4,8 @@ Changelog
 .. code-block:: text
 .. code-block:: text
 
 
     Version 1.3
     Version 1.3
+        *   Optimized LaTeX renderer. Now, it can also output a stepwise
+            trace of the system.
         *   Renamed CBD.py -> Core.py to prevent "from CBD.CBD import CBD"
         *   Renamed CBD.py -> Core.py to prevent "from CBD.CBD import CBD"
         +   Added simple equation to CBD converter: eq2CBD.
         +   Added simple equation to CBD converter: eq2CBD.
         *   Extracted simulation clock to custom block.
         *   Extracted simulation clock to custom block.

+ 17 - 16
examples/HarmonicOscilator/CBDA.py

@@ -150,25 +150,26 @@ class ErrorB(CBD):
 if __name__ == '__main__':
 if __name__ == '__main__':
 	outputs = []
 	outputs = []
 	signals = []
 	signals = []
-	for dt in [0.1, 0.01, 0.001]:
+	for dt in [0.1, 0.01]:
+		print("DT:", dt)
 		cbd = ErrorA("ErrorA", dt=dt)
 		cbd = ErrorA("ErrorA", dt=dt)
 		# Run the simulation
 		# Run the simulation
 		sim = Simulator(cbd)
 		sim = Simulator(cbd)
 		sim.setDeltaT(dt)
 		sim.setDeltaT(dt)
 		sim.run(int(50/dt))
 		sim.run(int(50/dt))
-		tvpl = cbd.getSignal("e")
-		outputs.append(tvpl)
-		signals.append(str(dt))
-		plot_signals(cbd, ['real', 'A'], f'Value A ({dt})')
-
-	plt.figure()
-	plt.title("Error A")
-	plt.xlabel('time')
-	plt.ylabel('N')
-	for i in range(3):
-		time = [x for x, _ in outputs[i]]
-		value = [x for _, x in outputs[i]]
-		plt.plot(time, value, label=signals[i])
-	plt.legend()
-	plt.show()
+		# tvpl = cbd.getSignal("e")
+		# outputs.append(tvpl)
+		# signals.append(str(dt))
+		# plot_signals(cbd, ['real', 'A'], f'Value A ({dt})')
+
+	# plt.figure()
+	# plt.title("Error A")
+	# plt.xlabel('time')
+	# plt.ylabel('N')
+	# for i in range(3):
+	# 	time = [x for x, _ in outputs[i]]
+	# 	value = [x for _, x in outputs[i]]
+	# 	plt.plot(time, value, label=signals[i])
+	# plt.legend()
+	# plt.show()
 
 

+ 101 - 34
src/CBD/converters/latexify.py

@@ -27,6 +27,22 @@ class CBD2Latex:
 		render_latex (bool):    When :code:`True`, the :func:`render` method will
 		render_latex (bool):    When :code:`True`, the :func:`render` method will
 								output a latex-formatted string. Otherwise, simple
 								output a latex-formatted string. Otherwise, simple
 								text formatting is done. Defaults to :code:`True`.
 								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):
 	def __init__(self, model, **kwargs):
 		self.model = model
 		self.model = model
@@ -35,7 +51,11 @@ class CBD2Latex:
 			"ignore_path": True,
 			"ignore_path": True,
 			"escape_nonlatex": True,
 			"escape_nonlatex": True,
 			"time_variable": 'i',
 			"time_variable": 'i',
-			"render_latex": True
+			"render_latex": True,
+			"time_format": "({time})",
+			"delta_t": "",
+			"replace_par": True,
+			"type_formats": {}
 		}
 		}
 
 
 		for k in kwargs:
 		for k in kwargs:
@@ -70,17 +90,27 @@ class CBD2Latex:
 			Technical Report <https://repository.uantwerpen.be/docman/irua/d28eb1/151279.pdf>`_
 			Technical Report <https://repository.uantwerpen.be/docman/irua/d28eb1/151279.pdf>`_
 		"""
 		"""
 		# Add all blocks
 		# Add all blocks
+		TF = self.config["type_formats"]
 		for block in self.model.getBlocks():
 		for block in self.model.getBlocks():
 			func = _BLOCK_MAP.get(block.getBlockType(), None)
 			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):
 			if isinstance(func, str):
 				func = lambda b, p, f=func: (p("OUT1"), Fnc(f, [p("%s") % x for x in block.getInputPortNames()]))
 				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))
 			res = func(block, lambda x: self._rename(block.getPath() + "." + x))
 			if isinstance(res, tuple):
 			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):
 			elif isinstance(res, list):
 				for r in res:
 				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
 		# Add all connections
 		for block in self.model.getBlocks():
 		for block in self.model.getBlocks():
@@ -106,6 +136,12 @@ class CBD2Latex:
 		if rl is None:
 		if rl is None:
 			rl = self.config["render_latex"]
 			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):
 		def apply_eq(var, val, ltx):
 			"""
 			"""
 			Applies a dictionary of equations.
 			Applies a dictionary of equations.
@@ -117,20 +153,22 @@ class CBD2Latex:
 			"""
 			"""
 			if isinstance(val, Fnc):
 			if isinstance(val, Fnc):
 				val = deepcopy(val)
 				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:
 				if len(val) > 2:
 					while val[0] == "(" and val[-1] == ")":
 					while val[0] == "(" and val[-1] == ")":
 						val = val[1:-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():
 		for variable, value in self.equations.items():
-			x = "\t{v}({time}) = {val}\n"
+			x = "\t{v}%s = {val}\n" % fmt
 			if rl:
 			if rl:
-				x = "\t" + r"{v}({time}) &=& {val}\\" + "\n"
+				x = "\t" + (r"{v}%s &=& {val}\\" % fmt) + "\n"
 			if not isinstance(value, list):
 			if not isinstance(value, list):
 				value = [value]
 				value = [value]
-			latex += apply_eq(variable, Fnc('+', value), x)
+			latex += apply_eq(variable, Fnc('+', value, TF), x)
 
 
 		ic = self.create_ic()
 		ic = self.create_ic()
 		for variable, value in ic.items():
 		for variable, value in ic.items():
@@ -169,9 +207,9 @@ class CBD2Latex:
 		for i in range(stop):
 		for i in range(stop):
 			eqs = deepcopy(self.equations)
 			eqs = deepcopy(self.equations)
 			for k, e in eqs.items():
 			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] = 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)
 					eqs[k].apply_delay(i)
 					for c, v in created.items():
 					for c, v in created.items():
 						eqs[k].apply(c, v)
 						eqs[k].apply(c, v)
@@ -181,7 +219,7 @@ class CBD2Latex:
 						old = eqs[k]
 						old = eqs[k]
 						if isinstance(eqs[k], Fnc):
 						if isinstance(eqs[k], Fnc):
 							eqs[k] = eqs[k].simplify()
 							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
 		return created
 
 
 	def simplify_links(self):
 	def simplify_links(self):
@@ -320,15 +358,24 @@ class Fnc:
 		name (str):     The name of the function.
 		name (str):     The name of the function.
 		args (list):    The ordered list of arguments to be applied by the
 		args (list):    The ordered list of arguments to be applied by the
 						function.
 						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.name = name
 		self.args = list(args)
 		self.args = list(args)
+		self.fmt = {} if fmt is None else fmt
 
 
 	def __repr__(self):
 	def __repr__(self):
 		return "%s%s" % (self.name, self.args)
 		return "%s%s" % (self.name, self.args)
 
 
 	def __str__(self):
 	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']:
 		if self.name in ['+', '*', '^', '%', '<', '<=', '==', 'or', 'and']:
 			return " {} ".format(self.name).join(["({})".format(str(x)) for x in self.args])
 			return " {} ".format(self.name).join(["({})".format(str(x)) for x in self.args])
 		elif self.name in ['-', '!']:
 		elif self.name in ['-', '!']:
@@ -370,7 +417,7 @@ class Fnc:
 		Args:
 		Args:
 			time (int): The time whence to apply the delay.
 			time (int): The time whence to apply the delay.
 		"""
 		"""
-		if self.name == 'D':
+		if self.name in _MEMORY:
 			if time == 0:
 			if time == 0:
 				return self.args[1]
 				return self.args[1]
 			elif time > 0:
 			elif time > 0:
@@ -406,7 +453,7 @@ class Fnc:
 				if c == 1:
 				if c == 1:
 					nargs.append(a)
 					nargs.append(a)
 				else:
 				else:
-					nargs.append(Fnc("*", [a, c]))
+					nargs.append(Fnc("*", [a, c], self.fmt))
 			if len(nargs) == 1:
 			if len(nargs) == 1:
 				return nargs[0]
 				return nargs[0]
 			if len(nargs) == 0:
 			if len(nargs) == 0:
@@ -428,7 +475,7 @@ class Fnc:
 				if c == 1:
 				if c == 1:
 					nargs.append(a)
 					nargs.append(a)
 				else:
 				else:
-					nargs.append(Fnc("^", [a, c]))
+					nargs.append(Fnc("^", [a, c], self.fmt))
 			if len(nargs) == 1:
 			if len(nargs) == 1:
 				return nargs[0]
 				return nargs[0]
 		elif name == '^':
 		elif name == '^':
@@ -514,7 +561,7 @@ class Fnc:
 			if self.args[0] == self.args[1]:
 			if self.args[0] == self.args[1]:
 				return self.args[0]
 				return self.args[0]
 
 
-		return Fnc(name, nargs)
+		return Fnc(name, nargs, self.fmt)
 
 
 	def is_numeric(self):
 	def is_numeric(self):
 		"""
 		"""
@@ -540,17 +587,18 @@ class Fnc:
 		"""
 		"""
 		return self.name in ["+", "-", "*", "~", "^", "root", "%", "or", "and", "==", "<=", "<"]
 		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.
 		Returns a LaTeX-formatted string of this function.
 
 
 		Args:
 		Args:
 			latex (bool):   Whether or not to use LaTeX-based strings.
 			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)
 		largs = deepcopy(self.args)
 		for i, a in enumerate(self.args):
 		for i, a in enumerate(self.args):
 			if isinstance(a, Fnc):
 			if isinstance(a, Fnc):
-				txt = a.latex()
+				txt = a.latex(latex, time_var)
 				if a.brackets():
 				if a.brackets():
 					largs[i] = "(%s)" % txt
 					largs[i] = "(%s)" % txt
 				else:
 				else:
@@ -560,9 +608,13 @@ class Fnc:
 			else:
 			else:
 				largs[i] = str(a)
 				largs[i] = str(a)
 
 
+		f = self.fmt.get(self.name, None)
+		if f is not None:
+			return f(self.name, largs)
+
 		opers = {}
 		opers = {}
 		if latex:
 		if latex:
-			op = {
+			opers = {
 				'*': r"\cdot ",
 				'*': r"\cdot ",
 				'or': r"\wedge ",
 				'or': r"\wedge ",
 				'and': r"\vee ",
 				'and': r"\vee ",
@@ -593,9 +645,14 @@ class Fnc:
 			return "%s %s %s" % (largs[0], op, largs[1])
 			return "%s %s %s" % (largs[0], op, largs[1])
 		elif self.name == 'D':
 		elif self.name == 'D':
 			return largs[0]
 			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))
 		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.
 		Converts all equations to functions that take a time-argument.
 		Delay blocks decrease the "time" annotation. This function is used
 		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
 			t (str/int):    The time variable name, or an integer indicative of
 							a specific time that must be applied. E.g., setting
 							a specific time that must be applied. E.g., setting
 							this value to 2 will apply the the formulas at time 2.
 							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
 			time += 1
 		to = t
 		to = t
 		if isinstance(t, str):
 		if isinstance(t, str):
 			if time > 0:
 			if time > 0:
-				t += "-%d" % time
+				t += "-%d%s" % (time, dt)
 			if time is None:
 			if time is None:
 				t = "0"
 				t = "0"
 		else:
 		else:
@@ -628,14 +691,14 @@ class Fnc:
 		for i, a in enumerate(self.args):
 		for i, a in enumerate(self.args):
 			if isinstance(a, str):
 			if isinstance(a, str):
 				if not isinstance(t, str) and t < 0:
 				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:
 				else:
-					self.args[i] = "%s(%s)" % (a, str(t))
+					self.args[i] = "%s%s" % (a, fmt.format(time=t))
 			elif isinstance(a, Fnc):
 			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:
 				else:
-					a.apply_time(time, to)
+					a.apply_time(time, to, fmt, dt)
 
 
 	def get_delay_depth(self, start=0):
 	def get_delay_depth(self, start=0):
 		"""
 		"""
@@ -645,7 +708,7 @@ class Fnc:
 			start (int):    Initial count value.
 			start (int):    Initial count value.
 		"""
 		"""
 		c = start
 		c = start
-		if self.name == 'D':
+		if self.name in _MEMORY:
 			c += 1
 			c += 1
 		v = [c]
 		v = [c]
 		for a in self.args:
 		for a in self.args:
@@ -689,10 +752,13 @@ _BLOCK_MAP = {
 	"OrBlock": 'or',
 	"OrBlock": 'or',
 	"AndBlock": 'and',
 	"AndBlock": 'and',
 	"DelayBlock": 'D',
 	"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__':
 if __name__ == '__main__':
 	from CBD.Core import CBD
 	from CBD.Core import CBD
@@ -717,7 +783,7 @@ if __name__ == '__main__':
 			# Create the Blocks
 			# Create the Blocks
 			self.addBlock(DelayBlock("delay1"))
 			self.addBlock(DelayBlock("delay1"))
 			self.addBlock(DelayBlock("delay2"))
 			self.addBlock(DelayBlock("delay2"))
-			self.addBlock(DelayBlock("delay3"))
+			self.addBlock(DerivatorBlock("delay3"))
 			self.addBlock(AdderBlock("sum"))
 			self.addBlock(AdderBlock("sum"))
 			self.addBlock(ConstantBlock("zero", value=(0)))
 			self.addBlock(ConstantBlock("zero", value=(0)))
 			self.addBlock(ConstantBlock("one", value=(1)))
 			self.addBlock(ConstantBlock("one", value=(1)))
@@ -738,6 +804,7 @@ if __name__ == '__main__':
 	ltx = CBD2Latex(FibonacciGen("fib"), render_latex=False, show_steps=True)
 	ltx = CBD2Latex(FibonacciGen("fib"), render_latex=False, show_steps=True)
 	# ltx.render()
 	# ltx.render()
 	ltx.simplify()
 	ltx.simplify()
+	print("----------------------------")
 	print(ltx.render())
 	print(ltx.render())
 	print("----------------------------")
 	print("----------------------------")
 	print(ltx.eq())
 	print(ltx.eq())

+ 1 - 1
src/CBD/lib/std.py

@@ -88,7 +88,7 @@ class AdderBlock(BaseBlock):
 		"""
 		"""
 		return self.__numberOfInputs
 		return self.__numberOfInputs
 
 
-
+from functools import reduce
 class ProductBlock(BaseBlock):
 class ProductBlock(BaseBlock):
 	"""
 	"""
 	The product block will multiply all the inputs
 	The product block will multiply all the inputs