Sfoglia il codice sorgente

Finalized textual eq2CBD

rparedis 4 anni fa
parent
commit
c4232795b6

+ 7 - 0
doc/CBD.converters.eq2CBD.rst

@@ -0,0 +1,7 @@
+CBD.converters.eq2CBD module
+============================
+
+.. automodule:: CBD.converters.eq2CBD
+    :members:
+    :undoc-members:
+    :show-inheritance:

+ 1 - 0
doc/CBD.converters.rst

@@ -13,4 +13,5 @@ Submodules
 
    CBD.converters.CBD2LaTeX
    CBD.converters.CBDDraw
+   CBD.converters.eq2CBD
 

+ 1 - 0
doc/changelog.rst

@@ -4,6 +4,7 @@ Changelog
 .. code-block:: text
 
     Version 1.3
+        +   Added simple equation to CBD converter: eq2CBD.
         *   Extracted simulation clock to custom block.
         -   Removed "old" Variable Step Size simulation system.
         +   Added Runge-Kutta preprocessor with generic Butcher Tableau.

+ 3 - 1
src/CBD/converters/CBDDraw.py

@@ -2,8 +2,8 @@
 Useful drawing function to easily draw a CBD model in Graphviz.
 """
 import re
-from CBD.lib.std import *
 from CBD.CBD import CBD
+from CBD.lib.std import *
 
 def draw(cbd, filename, colors=None):
 	"""
@@ -37,6 +37,8 @@ digraph graphname {
 		"""
 		if isinstance(block, ConstantBlock):
 			label = block.getBlockType() + " (" + block.getBlockName() + ")\\n" + str(block.getValue())
+		elif isinstance(block, GenericBlock):
+			label = block.getBlockType() + " (" + block.getBlockName() + ")\\n" + str(block.getBlockOperator())
 		else:
 			label = block.getBlockType() + " (" + block.getBlockName() + ")"
 

+ 23 - 14
src/CBD/converters/eq.lark

@@ -1,30 +1,39 @@
 // Equation grammar, used for converting textual syntax to CBD models
-// TODO: special function calls
-%import common.SIGNED_NUMBER    -> NUMBER
-%import common.NEWLINE          -> NEWLINE
+%import common.NUMBER
+%import common.NEWLINE
 %import common.WS_INLINE
 %ignore WS_INLINE
 
-?start  :   precon? eqn* eq NEWLINE*
-precon  :   WITH TIME AS var NEWLINE
-eq      :   var EQUALS oper
+start   :   eqn* eq NEWLINE*
+?eq     :   stmt
+stmt    :   VNAME EQUALS oper
 ?eqn    :   eq NEWLINE
-?poper  :   "(" oper ")"
-?oper   :   sum
+poper   :   "(" oper ")"
+?oper   :   sum | expr
 ?sum    :   prod ((ADD|MINUS) prod)*
-?prod   :   pow ((MUL|DIV) pow)*
+?prod   :   (pow ((MUL|DIV) pow)*)
+?expr   :   mod | bool
+mod     :   pow MOD pow
+bool    :   pow BOP pow
 ?pow    :   var (POW var)?
+neg     :   MINUS var
+not_    :   NOT var
 var     :   NUMBER
-        |   MINUS? VNAME
+        |   VNAME
+        |   func
+        |   neg
+        |   not_
         |   poper
+func    :   FNAME "(" oper ("," oper)* ")"
 
-WITH    :   "with"
-TIME    :   "time"
-AS      :   "as"
-EQUALS  :   "="
+EQUALS  :   "=" | ":="
 ADD     :   "+"
 MINUS   :   "-"
 MUL     :   "*"
 DIV     :   "/"
+MOD     :   "%" | "mod"
 POW     :   "^"
+BOP     :   "==" | "<" | "<=" | ">" | ">=" | "or" | "and" | "&&" | "||"
+NOT     :   "!" | "not" | "~"
 VNAME   :   /[a-zA-Z_][a-zA-Z0-9_]*/
+FNAME   :   /[a-zA-Z_][a-zA-Z0-9_]*/

+ 343 - 47
src/CBD/converters/eq2CBD.py

@@ -2,93 +2,389 @@
 Transforms equations/textual denotations to CBD models.
 """
 import os
-from lark import Lark, Transformer, Token
+from lark import Lark, Transformer, Token, ParseError
+
+__all__ = ['eq2CBD']
 
 class eq2CBD:
+	"""
+	Converts equations (textual denotation) into corresponding CBD models.
+
+	After instantiating this class, the :func:`parse` method may be called to
+	obtain the CBD model(s).
+
+	All equations must be in the form of :code:`<output> = <expression>`, where
+	:code:`<output>` identifies the name of an output of the resulting CBD and
+	:code:`<expression>` defines the corresponding logic. To prevent the creation
+	of an output, :code:`:=` may be used instead of :code:`=`. Multiple equations
+	can be listed, separated by newlines. Variables used in the expressions will
+	be linked to one another if needs be. When a variable is used without any
+	descriptive reference, it will be used as an input. For instance, the text
+	:code:`y = 6 * x` will be transformed into a CBD with a single output :code:`y`
+	and a single input :code:`x`.
+
+	Variable names must match the regex :code:`[a-zA-Z_][a-zA-Z0-9_]*` and must not
+	be any allowed function name (see below). The variable :code:`time` is reserved
+	and will be replaced by a :code:`TimeBlock`.
+
+	The following operations are allowed and transformed w.r.t. the standard library's
+	building blocks (:mod:`CBD.lib.std`). The order of operations applied is: parentheses,
+	function calls, exponents, multiplication/division and addition/subtraction.
+
+	- :code:`(A)`: Places sub-equation :code:`A` in parentheses, giving precedence on
+	  the computation of that equation.
+	- :code:`-A`: Negation of a variable or value. In the case of a constant value, no
+	  additional negator will be added to the CBD, **unless** explicitly requested by
+	  placing the value within parentheses: i.e. :code:`-(4)`.
+	- :code:`1/A`: Inversion of a variable or value. In the case of a constant value, no
+	  additional inverter will be added to the CBD, **unless** explicitly requested by
+	  placing the value within parentheses: i.e. :code:`1/(4)`.
+	- :code:`~A` or :code:`!A` or :code:`not A`: Adds a :code:`NotBlock` before sub-equation
+	  :code:`A`.
+	- :code:`A + B + C - D`: Sum of two (or more) sub-equations. Whenever a subtraction
+	  is encountered, it will be replaced by an addition of the negator and the other
+	  terms. In the case of a constant value, the same logic as mentioned above is applied.
+	- :code:`A * B * C / D`: Multiplication of two (or more) sub-equations. Whenever a
+	  division is encountered, it will be replaced by the multiplication of the inverted
+	  value and other factors. In the case of a constant value, the same logic as mentioned
+	  above is applied.
+	- :code:`A^B`: Raises sub-equation :code:`A` to the power of sub-equation :code:`B`.
+	- :code:`A % B` or :code:`A mod B`: Modulo-divides sub-equation :code:`A` by sub-equation
+	  :code:`B`.
+	- :code:`A == B`: Tests equality between sub-equations :code:`A` and :code:`B`.
+	- :code:`A <= B`: Tests inequality between sub-equations :code:`A` and :code:`B`. The
+	  :code:`LessThanOrEqualsBlock` will be used here.
+	- :code:`A < B`: Tests inequality between sub-equations :code:`A` and :code:`B`. The
+	  :code:`LessThanBlock` will be used here.
+	- :code:`A >= B`: Tests inequality between sub-equations :code:`A` and :code:`B`. Behind
+	  the scenes, this code will be handled as if it were :code:`B <= A`.
+	- :code:`A > B`: Tests inequality between sub-equations :code:`A` and :code:`B`. Behind
+	  the scenes, this code will be handled as if it were :code:`B < A`.
+	- :code:`A or B` or :code:`A || B`: Merges both :code:`A` and :code:`B` in an
+	  :code:`OrBlock`.
+	- :code:`A and B` or :code:`A && B`: Merges both :code:`A` and :code:`B` in an
+	  :code:`AndBlock`.
+	- :code:`f(A)`: executes function :code:`f` on sub-equation :code:`A`. Besides all
+	  single-argument functions from the :mod:`math` module (see the :class:`CBD.lib.std.GenericBlock`),
+	  the allowed functions (case-insensitive) are:
+
+	.. list-table::
+	   :widths: 30 30 40
+	   :header-rows: 1
+
+	   * - function
+	     - argument/input port count
+	     - CBD block
+	   * - :code:`int`
+	     - 1
+	     - :class:`CBD.lib.std.IntBlock`
+	   * - :code:`abs`
+	     - 1
+	     - :class:`CBD.lib.std.AbsBlock`
+	   * - :code:`root`
+	     - 2
+	     - :class:`CBD.lib.std.RootBlock`
+	   * - :code:`sqrt`
+	     - 1
+	     - :class:`CBD.lib.std.RootBlock` with second input fixed to 2
+	   * - :code:`clamp` or :code:`sat`
+	     - 3
+	     - :class:`CBD.lib.std.ClampBlock`
+	   * - :code:`mux`
+	     - 3 (last argument is the :code:`select` input)
+	     - :class:`CBD.lib.std.MultiplexerBlock`
+	   * - :code:`d`
+	     - 2 (second argument is the :code:`IC`)
+	     - :class:`CBD.lib.std.DelayBlock`
+	   * - :code:`der`
+	     - 3 (second argument is the :code:`IC`, third is the :code:`delta_t`)
+	     - :class:`CBD.lib.std.DerivatorBlock`
+	   * - :code:`i`
+	     - 3 (second argument is the :code:`IC`, third is the :code:`delta_t`)
+	     - :class:`CBD.lib.std.IntegratorBlock`
+	"""
 	def __init__(self):
 		filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "eq.lark")
 		with open(filename) as file:
 			contents = file.read()
-		parser = Lark(contents, parser="earley")
-		tree = parser.parse("y = 3 * (-z + -4)")
-		# print(tree.pretty())
+		self.parser = Lark(contents, parser="earley")
+
+	def parse(self, text):
+		tree = self.parser.parse(text)
 		transformer = EqTransformer()
-		print(transformer.transform(tree))
+		return transformer.transform(tree)
+
+
+from CBD.CBD import CBD
+from CBD.lib.std import *
+import math
+
+class EqFunctions:
+	def __init__(self, model):
+		self.model = model
+
+	def _has_func(self, fname):
+		return fname in [x for x in dir(self) if callable(getattr(self, x)) and not x.startswith("_")]
+
+	def int(self):
+		block = IntBlock("")
+		self.model.addBlock(block)
+		return block, 1
+
+	def abs(self):
+		block = AbsBlock("")
+		self.model.addBlock(block)
+		return block, 1
+
+	def root(self):
+		block = RootBlock("")
+		self.model.addBlock(block)
+		return block, 2
+
+	def sqrt(self):
+		two = ConstantBlock("", 2.0)
+		block = RootBlock("")
+		self.model.addBlock(two)
+		self.model.addBlock(block)
+		self.model.addConnection(two, block, "IN2")
+		return block, 1
+
+	def clamp(self):
+		block = ClampBlock("", use_const=False)
+		self.model.addBlock(block)
+		return block, 3
+
+	sat = clamp
+
+	def mux(self):
+		block = MultiplexerBlock("")
+		self.model.addBlock(block)
+		return block, 3, {"IN3": "select"}
 
-class Node:
-	def __init__(self, value, type):
-		self.value = value
-		self.type = type
-		self.conn = []
+	def d(self):
+		block = DelayBlock("")
+		self.model.addBlock(block)
+		return block, 2, {"IN2": "IC"}
+
+	def i(self):
+		block = IntegratorBlock("")
+		self.model.addBlock(block)
+		return block, 3, {"IN2": "IC", "IN3": "delta_t"}
+
+	def der(self):
+		block = DerivatorBlock("")
+		self.model.addBlock(block)
+		return block, 3, {"IN2": "IC", "IN3": "delta_t"}
 
-	def __repr__(self):
-		if len(self.conn) > 0:
-			return "%s(%s) %s" % (self.type, self.value, self.conn)
-		return "%s(%s)" % (self.type, self.value)
 
 class EqTransformer(Transformer):
+	# TODO: multiple output ports?
+	# TODO: constant folding
 	def __init__(self):
 		super().__init__()
+		self.model = CBD("")
+		self.functions = EqFunctions(self.model)
 		self.vars = {}
+		self.var_results = {}
+		self.nocollapse = set()
+
+	def link(self, what, to, opn=None, ipn=None):
+		if isinstance(what, Token) and what.type == "VNAME":
+			self.linkVar(what.value, (to, ipn))
+		else:
+			self.model.addConnection(what, to, ipn, opn)
+
+	def linkVar(self, name, to):
+		self.vars[name].append(to)
+
+	def start(self, _):
+		all_vars = set(self.vars.keys())
+		output_vars = set(self.var_results.keys())
+		input_vars = all_vars - output_vars
+		for inp in input_vars:
+			self.model.addInputPort(inp)
+			self.var_results[inp] = inp
+		for var, cons in self.vars.items():
+			from_ = self.var_results[var]
+			for con, ipn in cons:
+				self.model.addConnection(from_, con, ipn, None)
+		return self.model
 
-	def eq(self, items):
-		node = Node("=", "OUT")
-		node.conn.append(items[0])
-		node.conn.append(items[2])
-		return node
+	def eqn(self, items):
+		return items[0]
+
+	def stmt(self, items):
+		vname = items[0].value
+		self.var_results[vname] = items[2]
+		if items[1].value == "=":
+			self.model.addOutputPort(vname)
+			self.model.addConnection(items[2], vname)
+		return items[2]
+
+	def poper(self, items):
+		self.nocollapse.add(items[0].getBlockName())
+		return items[0]
 
 	def sum(self, items):
 		if len(items) > 1:
-			add = Node("+", "SUM")
-			add.conn.append(items[0])
-			for i in range((len(items) - 1) // 2):
+			N = ((len(items) - 1) // 2) + 1
+			block = AdderBlock("", N)
+			self.model.addBlock(block)
+			self.link(items[0], block)
+			for i in range(N - 1):
 				idx = (i * 2) + 1
 				if items[idx].type == "ADD":
-					add.conn.append(items[idx+1])
+					self.link(items[idx+1], block)
 				else:
-					neg = Node("-", "NEG")
-					neg.conn.append(items[idx+1])
-					add.conn.append(neg)
-			return add
+					neg = self.neg(["-", items[idx+1]])
+					self.link(neg, block)
+			return block
 		return items[0]
 
 	def prod(self, items):
 		if len(items) > 1:
-			mul = Node("*", "MUL")
-			mul.conn.append(items[0])
-			for i in range((len(items) - 1) // 2):
+			N = ((len(items) - 1) // 2) + 1
+			block = ProductBlock("", N)
+			self.model.addBlock(block)
+			self.link(items[0], block)
+			for i in range(N - 1):
 				idx = (i * 2) + 1
 				if items[idx].type == "MUL":
-					mul.conn.append(items[idx+1])
-				else:
-					inv = Node("/", "INV")
-					inv.conn.append(items[idx+1])
-					mul.conn.append(inv)
-			return mul
+					self.link(items[idx+1], block)
+				elif items[idx].type == "DIV":
+					inv = self.inv(["/", items[idx+1]])
+					self.link(inv, block)
+			return block
 		return items[0]
 
 	def pow(self, items):
 		if len(items) > 1:
-			pow = Node("^", "POW")
-			pow.conn.append(items[0])
+			block = PowerBlock("")
+			self.model.addBlock(block)
+			self.link(items[0], block)
 			if len(items) == 3:
-				pow.conn.append(items[2])
-			return pow
+				self.link(items[2], block, ipn="IN2")
+			return block
 		return items[0]
 
+	def mod(self, items):
+		block = ModuloBlock("")
+		self.model.addBlock(block)
+		self.link(items[0], block)
+		self.link(items[2], block, ipn="IN2")
+		return block
+
+	def neg(self, items):
+		if isinstance(items[1], ConstantBlock) \
+				and items[1].getBlockName() not in self.nocollapse:
+			items[1].setValue(-items[1].getValue())
+			return items[1]
+		block = NegatorBlock("")
+		self.model.addBlock(block)
+		self.link(items[1], block)
+		return block
+
+	def not_(self, items):
+		block = NotBlock("")
+		self.model.addBlock(block)
+		self.link(items[1], block)
+		return block
+
+	def delay(self, items):
+		block = DelayBlock("")
+		self.model.addBlock(block)
+		self.link(items[0], block)
+		return block
+
+	def inv(self, items):
+		if isinstance(items[1], ConstantBlock) \
+				and items[1].getBlockName() not in self.nocollapse:
+			items[1].setValue(1. / items[1].getValue())
+			return items[1]
+		block = InverterBlock("")
+		self.model.addBlock(block)
+		self.link(items[1], block)
+		return block
+
 	def var(self, items):
-		if len(items) == 2:
-			neg = Node("-", "NEG")
-			neg.conn.append(items[1])
-			return neg
 		return items[0]
 
+	def func(self, items):
+		fname = items[0].value.lower()
+		args = items[1:]
+		if self.functions._has_func(fname):
+			vals = getattr(self.functions, fname)()
+			block = vals[0]
+			acnt = vals[1]
+			mapper = {} if len(vals) == 2 else vals[2]
+			if acnt > len(args):
+				raise ParseError("Function '%s' has too few arguments (got %d, expected %d); "
+				                 "at line %d, column %d" % (fname, len(args), acnt, items[0].line, items[0].end_column))
+			if acnt < len(args):
+				raise ParseError("Function '%s' has too many arguments (got %d, expected %d); "
+				                 "at line %d, column %d" % (fname, len(args), acnt, items[0].line, items[0].end_column))
+			for i, arg in enumerate(args):
+				self.link(arg, block, ipn=mapper.get("IN%d" % (i+1), "IN%d" % (i+1)))
+			return block
+		if not hasattr(math, fname):
+			raise ParseError("Function '%s' does not exist at line %d, column %d" \
+			                    % (fname, items[0].line, items[0].end_column))
+		if len(args) > 1:
+			raise ParseError("Function '%s' has too many arguments (got %d, expected 1); "
+			                 "at line %d, column %d" % (fname, len(args), items[0].line, items[0].end_column))
+		block = GenericBlock("", fname)
+		self.model.addBlock(block)
+		self.link(args[0], block)
+		return block
+
+	def bool(self, items):
+		oper = items[1].value
+		first, second = items[0], items[2]
+		if oper == "==":
+			block = EqualsBlock("")
+		elif oper == "<=":
+			block = LessThanOrEqualsBlock("")
+		elif oper == "<":
+			block = LessThanBlock("")
+		elif oper == ">=":
+			block = LessThanOrEqualsBlock("")
+			first, second = second, first
+		elif oper == ">":
+			block = LessThanBlock("")
+			first, second = second, first
+		elif oper in ["or", "||"]:
+			block = OrBlock("")
+		elif oper in ["and", "&&"]:
+			block = AndBlock("")
+		else:
+			raise ValueError("Impossible condition: uncaught, invalid boolean operator!")
+
+		self.model.addBlock(block)
+		self.link(first, block, ipn="IN1")
+		self.link(second, block, ipn="IN2")
+		return block
+
 	def VNAME(self, tok):
-		return self.vars.setdefault(tok.value, Node(tok.value, tok.type))
+		vname = tok.value
+		if vname == "time":
+			block = TimeBlock("")
+			self.model.addBlock(block)
+			return block
+		if self.functions._has_func(vname):
+			raise ParseError("Invalid variable name '%s' at line %d, column %d" \
+			                 % (vname, items[0].line, items[0].end_column))
+		if vname not in self.vars:
+			self.vars[vname] = []
+		return tok
 
 	def NUMBER(self, tok):
-		return Node(float(tok.value), "NUMBER")
+		block = ConstantBlock("", float(tok.value))
+		self.model.addBlock(block)
+		return block
 
 
 if __name__ == '__main__':
-	eq2CBD()
+	from CBD.converters.CBDDraw import draw
+
+	parser = eq2CBD()
+	draw(parser.parse("x := 7 + 6"), "test.dot")

+ 9 - 0
src/CBD/converters/test.dot

@@ -0,0 +1,9 @@
+
+digraph graphname {
+ node_140327829132816 [label="ConstantBlock (F6cEAfyq)\n7.0"];
+node_140327829132624 [label="ConstantBlock (F6cEAfvq)\n6.0"];
+node_140327829132384 [label="AdderBlock (F6cEAfrG)"];
+node_140327829132816 -> node_140327829132384 [label="OUT1 / IN1"];
+node_140327829132624 -> node_140327829132384 [label="OUT1 / IN2"];
+
+}

+ 47 - 10
src/CBD/lib/std.py

@@ -8,7 +8,7 @@ import math
 __all__ = ['ConstantBlock', 'NegatorBlock', 'InverterBlock', 'AdderBlock', 'ProductBlock', 'ModuloBlock',
            'RootBlock', 'AbsBlock', 'IntBlock', 'ClampBlock', 'GenericBlock', 'MultiplexerBlock', 'LessThanBlock',
            'EqualsBlock', 'LessThanOrEqualsBlock', 'NotBlock', 'OrBlock', 'AndBlock', 'DelayBlock', 'LoggingBlock',
-           'AddOneBlock', 'DerivatorBlock', 'IntegratorBlock', 'SplitBlock', 'Clock', 'TimeBlock']
+           'AddOneBlock', 'DerivatorBlock', 'IntegratorBlock', 'SplitBlock', 'Clock', 'TimeBlock', 'PowerBlock']
 
 class ConstantBlock(BaseBlock):
 	"""
@@ -86,14 +86,23 @@ class AdderBlock(BaseBlock):
 
 class ProductBlock(BaseBlock):
 	"""
-	The product block will multiply the two inputs
+	The product block will multiply all the inputs
 	"""
-	def __init__(self, block_name):
-		BaseBlock.__init__(self, block_name, ["IN1", "IN2"], ["OUT1"])
+	def __init__(self, block_name, numberOfInputs=2):
+		BaseBlock.__init__(self, block_name, ["IN%d" % (x+1) for x in range(numberOfInputs)], ["OUT1"])
 
 	def compute(self, curIteration):
 		# TO IMPLEMENT
-		self.appendToSignal(self.getInputSignal(curIteration, "IN1").value * self.getInputSignal(curIteration, "IN2").value)
+		result = 1
+		for i in range(1, self.__numberOfInputs+1):
+			result *= self.getInputSignal(curIteration, "IN%d"%i).value
+		self.appendToSignal(result)
+
+	def getNumberOfInputs(self):
+		"""
+		Gets the total number of input ports.
+		"""
+		return self.__numberOfInputs
 
 
 class ModuloBlock(BaseBlock):
@@ -121,6 +130,18 @@ class RootBlock(BaseBlock):
 		self.appendToSignal(self.getInputSignal(curIteration, "IN1").value ** (1 / self.getInputSignal(curIteration, "IN2").value))
 
 
+class PowerBlock(BaseBlock):
+	"""
+	A basic block that computes IN1 to the IN2-th power
+	"""
+	def __init__(self, block_name):
+		BaseBlock.__init__(self, block_name, ["IN1", "IN2"], ["OUT1"])
+
+	def compute(self, curIteration):
+		# TO IMPLEMENT
+		self.appendToSignal(self.getInputSignal(curIteration, "IN1").value ** self.getInputSignal(curIteration, "IN2").value)
+
+
 class AbsBlock(BaseBlock):
 	"""
 	The abs block will output the absolute value of the input.
@@ -147,15 +168,31 @@ class IntBlock(BaseBlock):
 class ClampBlock(BaseBlock):
 	"""
 	The clamp block will clamp the input between min and max.
-	"""
-	def __init__(self, block_name, min=-1, max=1):
-		super().__init__(block_name, ["IN1"], ["OUT1"])
+
+	Args:
+		block_name (str):   The name of the block.
+		min (numeric):      The minimal value.
+		max (numeric):      The maximal value.
+		use_const (bool):   When :code:`True`, the :attr:`min` and :attr:`max`
+							values will be used. Otherwise, the minimal and
+							maximal values are expected as inputs 2 and 3,
+							respectively.
+	"""
+	def __init__(self, block_name, min=-1, max=1, use_const=True):
+		super().__init__(block_name, ["IN1"] if use_const else ["IN1", "IN2", "IN3"], ["OUT1"])
+		self._use_const = use_const
 		self.min = min
 		self.max = max
 
 	def compute(self, curIteration):
-		x = self.getInputSignal(curIteration).value
-		self.appendToSignal(min(max(x, self.min), self.max))
+		if self._use_const:
+			min_ = self.min
+			max_ = self.max
+		else:
+			min_ = self.getInputSignal(curIteration, "IN2").value
+			max_ = self.getInputSignal(curIteration, "IN3").value
+		x = self.getInputSignal(curIteration, "IN1").value
+		self.appendToSignal(min(max(x, min_), max_))
 
 
 class GenericBlock(BaseBlock):