瀏覽代碼

Documentation updates + Multiplexer has become Blend

rparedis 1 年之前
父節點
當前提交
8d7e2fe5a5
共有 7 個文件被更改,包括 121 次插入54 次删除
  1. 二進制
      CBD.zip
  2. 5 0
      doc/_static/style.css
  3. 1 1
      doc/conf.py
  4. 14 0
      src/pyCBD/Core.py
  5. 67 43
      src/pyCBD/lib/std.py
  6. 9 0
      src/pyCBD/scheduling.py
  7. 25 10
      src/test/stdCBDTest.py

二進制
CBD.zip


+ 5 - 0
doc/_static/style.css

@@ -85,6 +85,11 @@ article.catalyst-article .class table tbody tr td.field-body p.admonition-title
 	margin-right: auto;
 }
 
+div.tip {
+	background-color: #e4fcff;
+	border: 1px solid #6df;
+}
+
 /*
 div.math > p > img {
 	display: inline-block;

+ 1 - 1
doc/conf.py

@@ -25,7 +25,7 @@ copyright = '2020, Randy Paredis'
 author = 'Randy Paredis'
 
 # The short X.Y version
-version = '1.5'
+version = '1.6'
 # The full version, including alpha/beta/rc tags
 release = ''
 

+ 14 - 0
src/pyCBD/Core.py

@@ -620,6 +620,20 @@ class CBD(BaseBlock):
         """
         return self if self._parent is None else self._parent.getTopCBD()
 
+    def getDependencies(self, curIteration):
+        # First, find all ports needed in the curIteration, based on the depGraph of
+        #  the children.
+        deps = set()
+        for inp in self.getInputPorts():
+            for p in inp.getNextPortClosure():
+                 deps |= set(p.block.getDependencies(curIteration))
+        # Next, collect all dependencies for those ports.
+        res = []
+        for dep in deps:
+            if dep.getIncoming() is not None:
+                res.append(dep.getIncoming().source)
+        return res
+
     def flatten(self, ignore=None, psep="."):
         """
         Flatten the CBD inline and call recursively for all sub-CBDs.

+ 67 - 43
src/pyCBD/lib/std.py

@@ -1,5 +1,8 @@
 """
 This file contains the standard library for CBD building blocks.
+
+.. versionchanged:: 1.6
+	Replaced the :code:`MultiplexerBlock` with the :code:`BlendBlock`.
 """
 from pyCBD.Core import BaseBlock, CBD
 import math
@@ -12,7 +15,7 @@ __all__ = ['ConstantBlock', 'NegatorBlock', 'InverterBlock',
            'MinBlock', 'MaxBlock',
            'LessThanBlock', 'EqualsBlock', 'LessThanOrEqualsBlock',
            'NotBlock', 'OrBlock', 'AndBlock',
-           'MultiplexerBlock', 'SplitBlock',
+           'BlendBlock', 'SplitBlock',
            'DelayBlock', 'DeltaTBlock', 'TimeBlock', 'LoggingBlock',
            'AddOneBlock', 'DerivatorBlock', 'IntegratorBlock',
            'Clock', 'SequenceBlock']
@@ -204,6 +207,10 @@ class ModuloBlock(BaseBlock):
 	:Output Ports:
 		**OUT1** -- The remainder after division.
 
+	Warning:
+		The order of inputs is important in this block, so a default port name cannot be identified.
+		Will always raise a :code:`ValueError`.
+
 	See Also:
 		`math.fmod <https://docs.python.org/3.8/library/math.html#math.fmod>`_
 	"""
@@ -215,6 +222,7 @@ class ModuloBlock(BaseBlock):
 		self.appendToSignal(math.fmod(self.getInputSignal(curIteration, "IN1").value, self.getInputSignal(curIteration, "IN2").value))
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 
 
@@ -241,6 +249,10 @@ class RootBlock(BaseBlock):
 
 	Raises:
 		ZeroDivisionError: When the input is less than :code:`tolerance`.
+
+	Warning:
+		The order of inputs is important in this block, so a default port name cannot be identified.
+		Will always raise a :code:`ValueError`.
 	"""
 	def __init__(self, block_name, tolerance=1e-30):
 		BaseBlock.__init__(self, block_name, ["IN1", "IN2"], ["OUT1"])
@@ -253,6 +265,7 @@ class RootBlock(BaseBlock):
 		self.appendToSignal(self.getInputSignal(curIteration, "IN1").value ** (1 / input))
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 
 
@@ -273,6 +286,10 @@ class PowerBlock(BaseBlock):
 
 	:Output Ports:
 		**OUT1** -- The :code:`IN2`-th power of :code:`IN1`.
+
+	Warning:
+		The order of inputs is important in this block, so a default port name cannot be identified.
+		Will always raise a :code:`ValueError`.
 	"""
 	def __init__(self, block_name):
 		BaseBlock.__init__(self, block_name, ["IN1", "IN2"], ["OUT1"])
@@ -281,6 +298,7 @@ class PowerBlock(BaseBlock):
 		self.appendToSignal(self.getInputSignal(curIteration, "IN1").value ** self.getInputSignal(curIteration, "IN2").value)
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 
 
@@ -348,6 +366,11 @@ class ClampBlock(BaseBlock):
 
 	:Output Ports:
 		**OUT1** -- The clamped value.
+
+	Warning:
+		The order of operands is important in this block if constants are being used,
+		so a default port name cannot be identified in that case.
+		Will raise a :code:`ValueError` if invalid.
 	"""
 	def __init__(self, block_name, min=-1, max=1, use_const=True):
 		BaseBlock.__init__(self, block_name, ["IN1"] if use_const else ["IN1", "IN2", "IN3"], ["OUT1"])
@@ -366,6 +389,7 @@ class ClampBlock(BaseBlock):
 		self.appendToSignal(min(max(x, min_), max_))
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		if not self._use_const:
 			raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 		return super(ClampBlock, self).defaultInputPortNameIdentifier()
@@ -423,57 +447,54 @@ class GenericBlock(BaseBlock):
 		return repr
 
 
-class MultiplexerBlock(BaseBlock):
+class BlendBlock(BaseBlock):
 	"""
-	The multiplexer block will output the signal from an input, based on the index
-	:code:`select`.
+	The blend block will output the combination of :code:`IN1` and :code:`IN2`,
+	mixed for an :code:`IN3` amount. In other words:
+
+	.. math::
+		OUT1 = (IN3 \\cdot IN1) + ((1 - IN3) \\cdot IN2)
+
+	Commonly, this block is also known as a :code:`mix()` operation, where :code:`IN3`
+	represents the blending alpha. When :code:`IN3` is an integer, this block basically
+	becomes an inline multiplexer.
+
+	.. versionadded:: 1.6
+	   Replaces the :code:`MultiplexerBlock`, given that is an special case of
+	   a :code:`BlendBlock`.
 
 	Args:
 		block_name (str):       The name of the block.
-		numberOfInputs (int):   The amount of input ports to choose from. Defaults to 2.
-		zero (bool):            When :code:`True`, the index is zero-based. Otherwise,
-								the :code:`select` signal is interpreted as 1-indexed.
-								Defaults to :code:`True`.
 
 	:Input Ports:
-		- **select** -- The input index to choose from. When out of range, an exception
-		  is thrown.
-		- **IN1** -- The first option.
-		- **IN2** -- The second option.
-		- ...
+		- **IN1** -- The first value.
+		- **IN2** -- The second vale.
+		- **IN3** -- The amount of blending between the first value and the second.
+					 This is expected to be a value between 0 and 1. When 0, :code:`IN2`
+					 is output. When 1, :code:`IN1` is yielded.
 
 	:Output Ports:
 		**OUT1** -- The input to which the operation was applied.
 
 	Raises:
-		IndexError: When the :code:`select` input is out of range.
+		ValueError: When the :code:`IN3` input is out of range.
 
-	Note:
-		This block is not optimized in the simulator. Even though there can be a part of the
-		block that is not necessary to be computed, the dependency graph still includes both
-		inputs. While this is a definite optimization, recomputing the dependency graph at
-		each iteration is much less ideal.
+	Warning:
+		The order of operands is important in this block, so a default port name cannot be identified.
+		Will always raise a :code:`ValueError`.
 	"""
-	def __init__(self, block_name, numberOfInputs=2, zero=True):
-		BaseBlock.__init__(self, block_name, ["select"] + ["IN%i" % (x + 1) for x in range(numberOfInputs)], ["OUT1"])
-		self.__numberOfInputs = numberOfInputs
-		self.__zero = zero
+	def __init__(self, block_name):
+		BaseBlock.__init__(self, block_name, ["IN1", "IN2", "IN3"], ["OUT1"])
 
 	def compute(self, curIteration):
-		select = self.getInputSignal(curIteration, "select").value
-		if self.__zero:
-			select += 1
-		if select < 0 or select > self.__numberOfInputs:
-			raise IndexError("Select input out of range for block %s" % self.getPath())
-		self.appendToSignal(self.getInputSignal(curIteration, "IN%d" % select).value)
-
-	def getNumberOfInputs(self):
-		"""
-		Gets the number of input ports to choose from.
-		"""
-		return self.__numberOfInputs
+		a = self.getInputSignal(curIteration, "IN1").value
+		b = self.getInputSignal(curIteration, "IN2").value
+		alpha = self.getInputSignal(curIteration, "IN3").value
+		if alpha > 1 or alpha < 0: raise ValueError("IN3 input out of range; expected value between 0 and 1.")
+		self.appendToSignal(alpha * a + (1 - alpha) * b)
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 
 
@@ -592,7 +613,11 @@ class LessThanBlock(BaseBlock):
 	:Output Ports:
 		**OUT1** -- Yields a 1 when :code:`IN1` is smaller than :code:`IN2`, otherwise 0.
 		In Python, 0 is a *falsy* value and can therefore be used as boolean check, or as computation
-		value for other equation in the CBD. Similaryly, 1 is a *truthy* value.
+		value for other equation in the CBD. Similarly, 1 is a *truthy* value.
+
+	Warning:
+		The order of operands is important in this block, so a default port name cannot be identified.
+		Will always raise a :code:`ValueError`.
 
 	Tip:
 		To check the "greater than" relationship, you can swap the :code:`IN1` and :code:`IN2` inputs
@@ -609,6 +634,7 @@ class LessThanBlock(BaseBlock):
 		self.appendToSignal(1 if gisv("IN1") < gisv("IN2") else 0)
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 
 
@@ -657,6 +683,10 @@ class LessThanOrEqualsBlock(BaseBlock):
 		In Python, 0 is a *falsy* value and can therefore be used as boolean check, or as computation
 		value for other equation in the CBD. Similaryly, 1 is a *truthy* value.
 
+	Warning:
+		The order of operands is important in this block, so a default port name cannot be identified.
+		Will always raise a :code:`ValueError`.
+
 	Tip:
 		To check the "greater than or equal" relationship, you can swap the :code:`IN1` and :code:`IN2` inputs
 		around.
@@ -672,6 +702,7 @@ class LessThanOrEqualsBlock(BaseBlock):
 		self.appendToSignal(1 if gisv("IN1") <= gisv("IN2") else 0)
 
 	def defaultInputPortNameIdentifier(self):
+		""":meta private:"""
 		raise ValueError("The order of the operands is important for this block. Please provide a port name.")
 
 
@@ -1047,13 +1078,6 @@ class IntegratorBlock(CBD):
 		self.addConnection("sumState", "delayState", input_port_name="IN1")
 		self.addConnection("sumState", "OUT1")
 
-	def getDependencies(self, curIteration):
-		# Treat dependencies differently.
-		# For instance, at the first iteration (curIteration == 0), the block only depends on the IC;
-		if curIteration == 0:
-			return [self.getInputPortByName("IC").getIncoming().source]
-		return []
-
 
 class Clock(BaseBlock):
 	"""

+ 9 - 0
src/pyCBD/scheduling.py

@@ -118,6 +118,15 @@ class TopologicalScheduler(Scheduler):
 		hence, it was written by Marc Provost.
 	"""
 	def schedule(self, depGraph, curIt, time):
+		"""
+		Obtains the actual schedule. Note that the :meth:`obtain` method should be
+		called instead of this method to accurately obtain the current schedule.
+
+		Args:
+			depGraph (CBD.depGraph.DepGraph):   The dependency graph of the model.
+			curIt (int):                        The current iteration value.
+			time (float):                       The current simulation time (unused here).
+		"""
 		mapping = depGraph.getSemanticMapping()
 		components = []
 		sortedList = self.topoSort(mapping, depGraph)

+ 25 - 10
src/test/stdCBDTest.py

@@ -8,6 +8,7 @@ import unittest
 from pyCBD.Core import *
 from pyCBD.lib.std import *
 from pyCBD.simulator import Simulator
+from pyCBD.depGraph import createDepGraph
 
 NUM_DISCR_TIME_STEPS = 5
 
@@ -249,20 +250,21 @@ class StdCBDTestCase(unittest.TestCase):
 		self._run(5)
 		self.assertEqual(self._getSignal("clamp"), [max(min(float(x), 2.0), -1.0) for x in range(-2, 3)])
 
-	def testMultiplexerBlock(self):
-		a = list(range(10))
-		b = list(range(100, 90, -1))
-		s = [0, 1, 1, 0, 0, 0, 1, 1, 1, 0]
+	def testBlendBlock(self):
+		a = list(range(11))
+		b = list(range(100, 89, -1))
+		s = [x / 10 for x in range(0, 11)]
 		self.CBD.addBlock(SequenceBlock(block_name="c1", sequence=a))
 		self.CBD.addBlock(SequenceBlock(block_name="c2", sequence=b))
 		self.CBD.addBlock(SequenceBlock(block_name="c3", sequence=s))
-		self.CBD.addBlock(MultiplexerBlock(block_name="mux"))
+		self.CBD.addBlock(BlendBlock(block_name="mix"))
 
-		self.CBD.addConnection("c1", "mux", input_port_name="IN1")
-		self.CBD.addConnection("c2", "mux", input_port_name="IN2")
-		self.CBD.addConnection("c3", "mux", input_port_name="select")
-		self._run(10)
-		self.assertEqual(self._getSignal("mux"), [0, 99, 98, 3, 4, 5, 94, 93, 92, 9])
+		self.CBD.addConnection("c1", "mix", input_port_name="IN1")
+		self.CBD.addConnection("c2", "mix", input_port_name="IN2")
+		self.CBD.addConnection("c3", "mix", input_port_name="IN3")
+		self._run(len(a))
+		self.assertListEqual([round(x, 1) for x in self._getSignal("mix")],
+		                        [100, 89.2, 78.8, 68.8, 59.2, 50, 41.2, 32.8, 24.8, 17.2, 10])
 
 	def testIntBlock(self):
 		seq = [1.2, 2.2, 3.8, 4.7, 2.3, 6.6666]
@@ -506,6 +508,19 @@ class StdCBDTestCase(unittest.TestCase):
 		self.assertFalse(any([x > epsilon for x in error]), "Error too large.\n\tExpected: {}\n\tActual: {}"
 		                                                    "\n\tErrors: {}".format(actual, measured, error))
 
+	def testIntegratorBlockDepGraph(self):
+		self.CBD.addBlock(ConstantBlock(block_name="c", value=0.0))
+		self.CBD.addBlock(SequenceBlock(block_name="a", sequence=list(range(10))))
+
+		self.CBD.addBlock(IntegratorBlock(block_name="int"))
+		self.CBD.addConnection("c", "int", input_port_name="IC")
+		self.CBD.addConnection("a", "int")
+
+		deps0 = self.CBD.getBlockByName("int").getDependencies(0)
+		self.assertListEqual(deps0, [self.CBD.getBlockByName("c").getOutputPortByName("OUT1")])
+		deps1 = self.CBD.getBlockByName("int").getDependencies(1)
+		self.assertListEqual(deps1, [])
+
 	def testDelayBlock(self):
 		self.CBD.addBlock(ConstantBlock(block_name="c1", value=5.0))
 		self.CBD.addBlock(ConstantBlock(block_name="c2", value=3.0))