فهرست منبع

First work on Tracers (with communication time)

rparedis 2 سال پیش
والد
کامیت
d309179faf

+ 6 - 1
doc/changelog.rst

@@ -3,8 +3,13 @@ Changelog
 
 .. code-block:: text
 
-    Version 1.5
+    Version 1.6
+        + Added communication interval to the core to be taken into account
+          by tracers.
+        + Added CSV, MAT and VCD Tracers
         * Changed project name from CBD to pyCBD
+
+    Version 1.5
         * Changed how ports work. Instead of using PortBlocks, a custom Port
           (and Connection) class has been introduced.
             - InputPortBlock, WireBlock and OutputPortBlock were removed.

+ 16 - 6
examples/scripts/SinGen/SinGen_experiment.py

@@ -6,18 +6,28 @@ from SinGen import *
 from pyCBD.simulator import Simulator
 import matplotlib.pyplot as plt
 
+plt.style.use('seaborn')
+
 
 sinGen = SinGen("SinGen")
 sim = Simulator(sinGen)
 
 # Change this to change the Sin step size
-sim.setDeltaT(0.5)
+sim.setDeltaT(0.16)
+# sim.setCommunicationInterval(0.35)
+sim.setCustomTracer("tracerVCD", "VCDTracer", ("sine.vcd",))
 
 # The termination time can be set as argument to the run call
 sim.run(20.0)
 
-data = sinGen.getSignalHistory('OUT1')
-x, y = [x for x, _ in data], [y for _, y in data]
-
-plt.plot(x, y)
-plt.show()
+# data = sinGen.getSignalHistory('OUT1')
+# x, y = [x for x, _ in data], [y for _, y in data]
+# # plt.plot(x, y, '--', c='black', lw=0.5, label="Signal")
+# plt.scatter(x, y, 20, 'blue', 'o', label="Integration Points")
+#
+# import pandas as pd
+# df = pd.read_csv("sine.csv")
+# plt.scatter(df["time"], df["OUT1"], 15, 'red', 'X', label="Communication Points")
+#
+# plt.legend()
+# plt.show()

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1528 - 0
examples/scripts/SinGen/sine.vcd


+ 1 - 1
experiments/AGV/AGVEnv.py

@@ -81,7 +81,7 @@ class AGVEnv(gym.Env):
 		state = np.array(self.get_state(agv))
 		last_state = self.states[-1]
 		self.states.append(state)
-		self.time = sim.getTime()
+		self.time = sim.getTime(len(self.actions))
 
 		moment = self.physical[self.physical["time"] <= self.time].iloc[-1]
 		offset = self.euclidean(moment["x"], moment["y"], state[0], state[1])

+ 3 - 3
experiments/AGV/tracking.py

@@ -53,8 +53,8 @@ class TrackingSimulator:
 
 	def check(self, model, curIt):
 		x, y, w = self.get_state()
-		time = self.sim.getTime()
-		rtime = self.sim.getRelativeTime()
+		time = self.sim.getTime(curIt)
+		rtime = self.sim.getRelativeTime(curIt)
 		off, a = self.offset2(time)
 		if off > ERROR:
 			return True
@@ -71,7 +71,7 @@ class TrackingSimulator:
 			for i in tqdm(range(100)):
 				if back is None:
 					self.sim.run(end_time, time)
-					back = self.sim.getTime()
+					back = self.sim.getTime(i)
 					if len(self.traces) == 0 or len(self.traces[-1]) > 1:
 						self.traces.append(self.trace[:])
 						self.trace.clear()

+ 31 - 4
src/pyCBD/Core.py

@@ -353,7 +353,7 @@ class BaseBlock:
         """
         name_output = "OUT1" if name_output is None else name_output
         port = self.getOutputPortByName(name_output)
-        port.set(Signal(self.getClock().getTime(), value))
+        port.set(Signal(self.getClock().getTime(port.count()), value))
 
     def getSignalHistory(self, name_output=None):
         """
@@ -419,13 +419,14 @@ class BaseBlock:
         """
         return self.getInputPortByName(input_port).getHistory()[curIteration]
 
-    def getPath(self, sep='.'):
+    def getPath(self, sep='.', ignore_parent=False):
         """Gets the path of the current block.
         This includes the paths from its parents. When the block has no parents
         i.e. when it's the top-level block, the block's name is returned.
 
         Args:
-            sep (str):  The separator to use. Defaults to :code:`.`
+            sep (str):              The separator to use. Defaults to :code:`.`
+            ignore_parent (bool):   Whether or not to ignore the root block name.
 
         Returns:
             The full path as a string.
@@ -436,8 +437,13 @@ class BaseBlock:
             that in its turn is located in this CBD has a path of :code:`child.grandchild`.
         """
         if self._parent is None:
+            if ignore_parent:
+                return ""
             return self.getBlockName()
-        return self._parent.getPath() + sep + self.getBlockName()
+        parpath = self._parent.getPath(sep, ignore_parent)
+        if len(parpath) == 0:
+            return self.getBlockName()
+        return parpath + sep + self.getBlockName()
 
     def compute(self, curIteration):
         """
@@ -915,6 +921,27 @@ class CBD(BaseBlock):
             block.clearPorts()
         self.clearPorts()
 
+    def getAllSignalNames(self, sep="."):
+        res = []
+        for block in self.getBlocks():
+            if isinstance(block, CBD):
+                res.extend(block.getAllSignalNames(sep))
+            for out in block.getOutputPorts():
+                path = block.getPath(sep, True)
+                if len(path) == 0:
+                    path = out.name
+                else:
+                    path += sep + out.name
+                res.append(path)
+        for out in self.getOutputPorts():
+            path = self.getPath(sep, True)
+            if len(path) == 0:
+                path = out.name
+            else:
+                path += sep + out.name
+            res.append(path)
+        return res
+
     def compute(self, curIteration):
         pass
 

+ 1 - 1
src/pyCBD/lib/endpoints.py

@@ -56,7 +56,7 @@ class SignalCollectorBlock(CollectorBlock):
 		self.buffer_size = buffer_size
 
 	def compute(self, curIteration):
-		time = self.getClock().getTime()
+		time = self.getClock().getTime(curIteration)
 		value = self.getInputSignal(curIteration, "IN1").value
 		self._data.append((time, value))
 		if self.buffer_size > 0:

+ 2 - 2
src/pyCBD/lib/io.py

@@ -154,7 +154,7 @@ class ReadCSV(BaseBlock):
 			raise ValueError("A repeating CSV series must not start at time 0")
 
 	def compute(self, curIteration):
-		time = self.getClock().getTime()
+		time = self.getClock().getTime(curIteration)
 		T = self.data[self.time_col][-1]
 		L = len(self.data[self.time_col])
 		data = {k: self.data[k] for k in self.data if k != self.time_col}
@@ -240,6 +240,6 @@ class WriteCSV(BaseBlock):
 
 	def compute(self, curIteration):
 		inputs = { col: self.getInputSignal(curIteration, col).value for col in self.columns if col != self.time_col }
-		inputs[self.time_col] = self.getClock().getTime()
+		inputs[self.time_col] = self.getClock().getTime(curIteration)
 
 		self.writer.writerow(inputs)

+ 29 - 12
src/pyCBD/lib/std.py

@@ -876,8 +876,8 @@ class TimeBlock(BaseBlock):
 		BaseBlock.__init__(self, block_name, [], ["OUT1", "relative"])
 
 	def compute(self, curIteration):
-		time = self.getClock().getTime()
-		rel_time = self.getClock().getRelativeTime()
+		time = self.getClock().getTime(curIteration)
+		rel_time = self.getClock().getRelativeTime(curIteration)
 		self.appendToSignal(time)
 		self.appendToSignal(rel_time, "relative")
 
@@ -907,7 +907,7 @@ class LoggingBlock(BaseBlock):
 
 	def compute(self, curIteration):
 		if self.getInputSignal(curIteration, "IN1").value:
-			simtime = str(self.getClock().getTime())
+			simtime = str(self.getClock().getTime(curIteration))
 			if self.__lev == logging.WARNING:
 				self.__logger.warning("[" + simtime + "]  " + self.__string, extra={"block": self})
 			elif self.__lev == logging.ERROR:
@@ -1072,25 +1072,42 @@ class Clock(BaseBlock):
 		return []
 
 	def compute(self, curIteration):
+		self.appendToSignal(self.__time, "time")
+		self.appendToSignal(self.getRelativeTime(curIteration), "rel_time")
+		self.appendToSignal(self.__delta, "delta_t")
+
 		if curIteration > 0:
 			self.__delta = self.getInputSignal(curIteration - 1, "h").value
 		self.__time += self.__delta
 
-		self.appendToSignal(self.__time, "time")
-		self.appendToSignal(self.getRelativeTime(), "rel_time")
-		self.appendToSignal(self.__delta, "delta_t")
-
-	def getTime(self):
+	def getStartTime(self):
+		"""
+		Gets the starting time of the model.
 		"""
-		Gets the current time of the clock.
+		return self.__start_time
+
+	def getTime(self, curIt):
 		"""
-		return self.__time
+		Gets the time of the clock at a certain iteration.
 
-	def getRelativeTime(self):
+		Args:
+			curIt (int):    The iteration to look at.
+		"""
+		thist = self.getSignalHistory("time")
+		if len(thist) == 0:
+			return self.__time
+		if len(thist) <= curIt:
+			return thist[-1][1] + self.__delta
+		return thist[curIt][1]
+
+	def getRelativeTime(self, curIt):
 		"""
 		Gets the relative simulation time (ignoring the start time).
+
+		Args:
+			curIt (int):    The iteration to look at.
 		"""
-		return self.getTime() - self.__start_time
+		return self.getTime(curIt) - self.__start_time
 
 	def setStartTime(self, start_time=0.0):
 		self.__start_time = start_time

+ 40 - 5
src/pyCBD/simulator.py

@@ -5,6 +5,7 @@ from pyCBD.loopsolvers.linearsolver import LinearSolver
 from pyCBD.realtime.threadingBackend import ThreadingBackend, Platform
 from pyCBD.scheduling import TopologicalScheduler
 from pyCBD.tracers import Tracers
+from pyCBD.tracers.interpolator import Interpolator
 from pyCBD.state_events.locators import RegulaFalsiStateEventLocator
 import pyCBD.realtime.accurate_time as time
 
@@ -69,6 +70,7 @@ class Simulator:
 		self.model = model
 
 		self.__deltaT = 1.0
+		self.__communication_interval = None
 		self.__realtime = False
 		self.__finished = True
 		self.__stop_requested = False
@@ -88,7 +90,6 @@ class Simulator:
 		# simulation data [dep graph, strong components, curIt]
 		self.__sim_data = [None, None, 0]
 
-		# self.__stepsize_backend = Fixed(self.__deltaT)
 		self.__scheduler = TopologicalScheduler()
 
 		self.__threading_backend = None
@@ -135,8 +136,12 @@ class Simulator:
 			self.__termination_time = term_time
 
 		if self.getClock() is None:
-			self.model.addFixedRateClock("clock", self.__deltaT)
+			self.model.addFixedRateClock(self.model.getUniqueBlockName("clock"), self.__deltaT)
 
+		interp = None
+		if self.__communication_interval is not None:
+			interp = Interpolator(ci=self.__communication_interval, start_time=self.getClock().getStartTime())
+		self.__tracer.startTracers(interp, self.model.getBlockName())
 		self.__sim_data = [None, None, 0]
 		self.__progress_finished = False
 		if self.__threading_backend is None:
@@ -235,7 +240,7 @@ class Simulator:
 			- :func:`setDeltaT`
 			- :class:`pyCBD.lib.std.Clock`
 		"""
-		return self.getClock().getTime()
+		return self.getClock().getTime(self.__sim_data[2])
 
 	def getRelativeTime(self):
 		"""
@@ -248,7 +253,7 @@ class Simulator:
 			- :func:`setDeltaT`
 			- :class:`pyCBD.lib.std.Clock`
 		"""
-		return self.getClock().getRelativeTime()
+		return self.getClock().getRelativeTime(self.__sim_data[2])
 
 	def getDeltaT(self):
 		"""
@@ -284,10 +289,38 @@ class Simulator:
 			- :func:`getRelativeTime`
 			- :func:`getDeltaT`
 			- :class:`pyCBD.lib.std.Clock`
+			- :func:`setCommunicationInterval`
 			- :func:`setStepSize`
 		"""
 		self.__deltaT = delta_t
 
+	def setCommunicationInterval(self, delta):
+		"""
+		Sets the time delta at which the information is communicated to the user.
+		When :code:`None`, the integration interval (i.e., delta t) will be used
+		for this.
+
+		Args:
+			delta (float):  The delta. When :code:`None`, this value will be unset.
+
+		Note:
+			This function will only influence the tracers that make use of this
+			feature. This has to do with what is communicated to the user and
+			not to when the actual computations happen.
+
+		.. versionadded:: 1.6
+
+		See Also:
+			- :func:`getClock`
+			- :func:`getTime`
+			- :func:`getRelativeTime`
+			- :func:`getDeltaT`
+			- :func:`setDeltaT`
+			- :class:`pyCBD.lib.std.Clock`
+			- :func:`setStepSize`
+		"""
+		self.__communication_interval = delta
+
 	def setScheduler(self, scheduler):
 		"""
 		Sets the scheduler for the simulation. It will identify the
@@ -518,7 +551,7 @@ class Simulator:
 		simT = self.getTime()
 		self.signal("prestep", pre, simT)
 		curIt = self.__sim_data[2]
-		self.__tracer.trace(self.__tracer.traceNewIteration, (curIt, simT))
+		self.__tracer.trace(self.__tracer.traceStartNewIteration, (curIt, simT))
 
 		# Efficiency reasons: dep graph only changes at these times
 		#   in the given set of library blocks.
@@ -554,6 +587,7 @@ class Simulator:
 			self.__sim_data[0] = None
 			self.__sim_data[2] = 0
 		post = time.time()
+		self.__tracer.trace(self.__tracer.traceEndNewIteration, (curIt, simT))
 		self.signal("poststep", pre, post, self.getTime())
 
 	def _lcc_compute(self):
@@ -650,6 +684,7 @@ class Simulator:
 						blockIndex = component.index(block)
 						block.appendToSignal(solutionVector[blockIndex])
 						self.__tracer.trace(self.__tracer.traceCompute, (curIteration, block))
+		self.__tracer.trace(self.__tracer.traceCompute, (curIteration, self.model))
 
 	def __hasCycle(self, component, depGraph):
 		"""

+ 35 - 4
src/pyCBD/tracers/__init__.py

@@ -1,6 +1,7 @@
 """
 The tracers module provides an interface for tracing simulation data.
 """
+import copy
 import time
 
 from pyCBD.tracers.baseTracer import BaseTracer
@@ -19,6 +20,7 @@ class Tracers:
 	def __init__(self, sim):
 		self.uid = 0
 		self.tracers = {}
+		self.recovers = {}
 		self.bus = []
 		self.sim = sim
 
@@ -53,7 +55,7 @@ class Tracers:
 
 		Args:
 			tracer:         Either a tuple of :code:`(file, classname, [args])`,
-							similar to `PythonPDEVS <http://msdl.cs.mcgill.ca/projects/DEVS/PythonPDEVS>`_;
+							similar to `PythonPDEVS <http://msdl.uantwerpen.be/projects/DEVS/PythonPDEVS>`_;
 							or an instance of a subclass of :class:`pyCBD.tracers.baseTracer.BaseTracer`.
 			recover (bool): Whether or not this is a recovered registration; i.e. whether or not the trace
 			                file should be appended. Defaults to :code:`False`.
@@ -67,7 +69,7 @@ class Tracers:
 		elif isinstance(tracer, BaseTracer):
 			tracer.uid = self.uid
 			self.tracers[self.uid] = tracer
-		self.tracers[self.uid].startTracer(recover)
+		self.recovers[self.uid] = recover
 		self.uid += 1
 
 	def deregisterTracer(self, uid):
@@ -81,6 +83,24 @@ class Tracers:
 			self.tracers[uid].stopTracer()
 			del self.tracers[uid]
 
+	def startTracers(self, interp=None, model_name="model"):
+		"""
+		Starts the tracers.
+
+		Args:
+			interp (pyCBD.tracers.interpolator.Interpolator):   The interpolator to use for the tracers.
+			model_name (str):       The name of the model.
+
+		Returns:
+
+		"""
+		for tid in self.tracers:
+			self.tracers[tid].setModelName(model_name)
+			if interp is not None:
+				self.tracers[tid].setInterpolator(copy.deepcopy(interp))
+			self.tracers[tid].getInterpolator().set_header(self.sim.model.getAllSignalNames())
+			self.tracers[tid].startTracer(self.recovers[tid])
+
 	def stopTracers(self):
 		"""
 		Stops all tracers.
@@ -105,7 +125,7 @@ class Tracers:
 			return self.tracers[uid]
 		raise ValueError("No such tracer %d." % uid)
 
-	def traceNewIteration(self, curIt, time):
+	def traceStartNewIteration(self, curIt, time):
 		"""
 		Traces a new iteration start.
 
@@ -114,7 +134,18 @@ class Tracers:
 			time (numeric): The current simulation time.
 		"""
 		for tracer in self.tracers.values():
-			tracer.traceNewIteration(curIt, time)
+			tracer.traceStartNewIteration(curIt, time)
+
+	def traceEndNewIteration(self, curIt, time):
+		"""
+		Traces a new iteration end.
+
+		Args:
+			curIt (int):    The current iteration.
+			time (numeric): The current simulation time.
+		"""
+		for tracer in self.tracers.values():
+			tracer.traceEndNewIteration(curIt, time)
 
 	def traceCompute(self, curIteration, block):
 		"""

+ 41 - 5
src/pyCBD/tracers/baseTracer.py

@@ -2,7 +2,7 @@ import sys
 import time
 from pyCBD.realtime import accurate_time
 from .color import COLOR
-
+from .interpolator import Interpolator
 
 class BaseTracer:
 	"""
@@ -25,6 +25,23 @@ class BaseTracer:
 		self.file = None
 		self.width = 80
 		self.__active = False
+		self._model_name = "model"
+		self._interpolator = Interpolator()
+
+	def setModelName(self, model_name):
+		"""
+		Sets a model name for the tracer.
+
+		Args:
+			model_name (str):   The CBD model name
+		"""
+		self._model_name = model_name
+
+	def setInterpolator(self, interpolator):
+		self._interpolator = interpolator
+
+	def getInterpolator(self):
+		return self._interpolator
 
 	def openFile(self, recover=False):
 		"""
@@ -76,7 +93,7 @@ class BaseTracer:
 			self.__active = False
 			self.closeFile()
 
-	def traceNewIteration(self, curIt, time):
+	def traceStartNewIteration(self, curIt, time):
 		"""
 		Traces the start of a new iteration.
 
@@ -87,7 +104,20 @@ class BaseTracer:
 			curIt (int):    The current iteration.
 			time (numeric): The current simulation time.
 		"""
-		raise NotImplementedError()
+		pass
+
+	def traceEndIteration(self, curIt, time):
+		"""
+		Traces the end of a new iteration.
+
+		Note:
+			This function must be implemented in the subclass(es)!
+
+		Args:
+			curIt (int):    The current iteration.
+			time (numeric): The current simulation time.
+		"""
+		pass
 
 	def traceCompute(self, curIt, block):
 		"""
@@ -100,7 +130,13 @@ class BaseTracer:
 			curIt (int):                The current iteration.
 			block (CBD.Core.BaseBlock): The block for which a compute just happened.
 		"""
-		raise NotImplementedError()
+		for out in block.getOutputPorts():
+			path = block.getPath(".", True)
+			if len(path) == 0:
+				path = out.name
+			else:
+				path += "." + out.name
+			self._interpolator.put_signal(path, out.getHistory()[curIt])
 
 	def trace(self, *text):
 		"""
@@ -135,4 +171,4 @@ class BaseTracer:
 		See Also:
 			`Documentation on time formatting. <https://docs.python.org/3/library/time.html#time.strftime>`_
 		"""
-		return time.strftime(format, accurate_time.time())
+		return time.strftime(format, time.gmtime(accurate_time.time()))

+ 198 - 0
src/pyCBD/tracers/interpolator.py

@@ -0,0 +1,198 @@
+import math
+
+
+def lerp(x0, y0, x1, y1, x):
+	"""
+	Linear interpolation method.
+
+	Args:
+		x0: first x-coordinate
+		y0: first y-coordinate
+		x1: second x-coordinate
+		y1: second y-coordinate
+		x: point to interpolate at
+
+	Returns:
+		The interpolated y-value at x for the line segment between (x0, y0) and (x1, y1).
+
+	Raises:
+		AssertionError when the value is outside the boundaries, or when the boundaries
+		are invalid.
+	"""
+	if x <= x0:
+		return y0
+	if x1 <= x:
+		return y1
+	return (y1 - y0) / (x1 - x0) * (x - x0) + y0
+
+
+class Interpolator:
+	"""
+	Interpolation class to be used when a communication interval is set.
+
+	Args:
+		ci (numeric):           Communication interval. This is the fixed rate at
+								which data is communicated to the end-user.
+		method (callable):      Method for the interpolation. Must be a function
+								that takes 2 coordinates and another x-value to
+								interpolate at in the form of
+								:code:`x0, y0, x1, y1, x`. This must return a
+								single value. Defaults to :meth:`Interpolator.linear`.
+		start_time (numeric):   Starting time for the simulation. Defaults to 0.
+	"""
+	def __init__(self, ci=0.0, method=lerp, start_time=0.0):
+		self.__ci = ci
+		self.__header = []
+		self.__method = method
+		self.__start_time = start_time
+
+		self.__last_time = self.__start_time - self.__ci
+		self.__last_signals = {}
+		self.__curr_signals = {}
+
+	def __repr__(self):
+		return "Interpolator <%.2f> %s" % (self.__ci, str(self.__header))
+
+	def set_header(self, header):
+		"""
+		Sets the ordering of signals.
+
+		Args:
+			header (list):  Ordered list with all the signals to take into account.
+		"""
+		self.__header = header
+
+	def get_header(self):
+		"""
+		Gets the current ordering of the signals.
+		"""
+		return self.__header
+
+	def is_ci_set(self):
+		"""
+		Checks if the communication interval is set. Otherwise, normal signal outputs will be
+		yielded.
+
+		Returns:
+			:code:`True` if the communication interval is set.
+		"""
+		return self.__ci > 0
+
+	def put_signal(self, symbol, signal):
+		"""
+		Adds a signal to interpolate.
+
+		Args:
+			symbol (str):   The symbol to look at.
+			signal (tuple): The signal to store.
+		"""
+		assert symbol in self.__header, "Can only add a signal that is in the header;\n\t%s not found in %s." %(symbol, str(self.__header))
+		self.__curr_signals[symbol] = signal
+		if symbol not in self.__last_signals:
+			self.__last_signals[symbol] = signal
+
+	def get_curr_signal(self, symbol):
+		"""
+		Gets the current signal for a certain symbol.
+
+		Args:
+			symbol (str):   The symbol to look at.
+		"""
+		return self.__curr_signals[symbol]
+
+	def is_delta_passed(self, current_time):
+		"""
+		Checks if the delta has passed too much.
+
+		Args:
+			current_time (numeric): The current time of the simulation.
+
+		Returns:
+			:code:`True` if a new :meth:`compute` must be called.
+		"""
+		return (current_time - self.__last_time) >= self.__ci
+
+	def get_deltas_passed(self, current_time):
+		"""
+		Get how many deltas have passed since the last iteration computation.
+		This is the amount of values to output.
+
+		Args:
+			current_time (numeric): The current time of the simulation.
+
+		Returns:
+			How many iterations have passed since the last computation.
+		"""
+		return math.floor((current_time - self.__last_time) / self.__ci)
+
+	def get_closest_time(self, current_time):
+		"""
+		Compute the closest time in communication intervals.
+
+		Args:
+			current_time (numeric): The current simulation time.
+
+		Returns:
+			The closest communication interval value.
+		"""
+		return current_time - math.fmod(current_time, self.__ci)
+
+	def get_next_computation_point(self):
+		"""
+		Get the next point at which the compute must be called.
+		"""
+		return self.__last_time + self.__ci
+
+	def get_precision(self):
+		"""
+		Obtains the communication interval precision as a multitude of 3.
+
+		Warning:
+			The simulator is accurate to the microsecond-level. A value
+			higher than 6 might yield undefined behaviour.
+
+		Returns:
+			An exponent that indicates the precision of the communication
+			interval.
+
+			- 0 means 'seconds'
+			- 3 means 'milliseconds'
+			- 6 means 'microseconds'
+			- 9 means 'nanoseconds'
+			- 12 means 'picoseconds'
+			- 15 means 'femtoseconds'
+		"""
+		res = int(math.log10(self.__ci)) + 1
+		res = math.ceil(res / 3) * 3
+		return min(res, 15)
+
+	def compute(self, x):
+		"""
+		Computes all the values that must be outputted.
+
+		Args:
+			x (numeric):    The next simulation time.
+
+		Returns:
+			An ordered list with all values that must be outputted in this
+			iteration.
+		"""
+		output = []
+		for sym in self.__header:
+			t0, v0 = self.__last_signals[sym]
+			t1, v1 = self.__curr_signals[sym]
+			output.append(self.__method(t0, v0, t1, v1, x))
+		return output
+
+	def update_time(self):
+		"""
+		Updates the time after a computation.
+		"""
+		self.__last_time += self.__ci
+
+	def post_compute(self):
+		"""
+		Function to execute at each iteration, but after the compute.
+		"""
+		self.__last_signals = self.__curr_signals.copy()
+		self.__curr_signals.clear()

+ 35 - 0
src/pyCBD/tracers/tracerCSV.py

@@ -0,0 +1,35 @@
+"""
+CSV tracer for the CBD Simulator.
+"""
+
+from .baseTracer import BaseTracer
+
+class CSVTracer(BaseTracer):
+	"""
+	CSV tracer for the CBD Simulator.
+
+	Note:
+		During the simulation, all intermediary results will be constantly
+		written to the file.
+	"""
+	def startTracer(self, recover=False):
+		super().startTracer(recover)
+		self.traceln(",".join(["time"] + self._interpolator.get_header()))
+
+	def traceEndNewIteration(self, _, time):
+		if self._interpolator.is_ci_set():
+			cnt = self._interpolator.get_deltas_passed(time)
+			for i in range(cnt):
+				x = self._interpolator.get_next_computation_point()
+				if x <= time:
+					vals = self._interpolator.compute(x)
+					self.traceln(",".join([str(x)] + [str(v) for v in vals]))
+				self._interpolator.update_time()
+			self._interpolator.post_compute()
+		else:
+			# FIXME: what when multi-rate simulation?
+			res = [time]
+			for sig in self._interpolator.get_header():
+				res.append(self._interpolator.get_curr_signal(sig)[1])
+			self.traceln(",".join([str(x) for x in res]))
+

+ 53 - 0
src/pyCBD/tracers/tracerMAT.py

@@ -0,0 +1,53 @@
+"""
+Matlab file tracer for the CBD Simulator.
+"""
+
+from .baseTracer import BaseTracer
+import re
+
+try:
+	import scipy.io as sio
+
+	class MatTracer(BaseTracer):
+		"""
+		Matlab tracer for the CBD Simulator.
+
+		Note:
+			Because of the Matlab file compression, no intermediary files will
+			be opened or written to during the simulation.
+		"""
+		def __init__(self, uid=-1, filename=None):
+			super().__init__(uid, filename)
+			self.__data = {"time": []}
+
+		def startTracer(self, recover=False):
+			for h in self._interpolator.get_header():
+				self.__data[h] = []
+
+		def traceEndNewIteration(self, _, time):
+			if self._interpolator.is_ci_set():
+				cnt = self._interpolator.get_deltas_passed(time)
+				for i in range(cnt):
+					x = self._interpolator.get_next_computation_point()
+					if x <= time:
+						vals = self._interpolator.compute(x)
+						self.__data["time"].append(x)
+						for hix, h in enumerate(self._interpolator.get_header()):
+							self.__data[h].append(vals[hix])
+					self._interpolator.update_time()
+				self._interpolator.post_compute()
+			else:
+				# FIXME: what when multi-rate simulation?
+				self.__data["time"].append(time)
+				for sig in self._interpolator.get_header():
+					self.__data[sig].append(self._interpolator.get_curr_signal(sig)[1])
+
+		def stopTracer(self):
+			data = {}
+			for k, v in self.__data.items():
+				data[re.sub('[^0-9a-zA-Z]+', "_", k)] = v
+			sio.savemat(self.filename, data)
+
+except ImportError:
+	raise ImportError("Can not import MatTracer without scipy.")
+

+ 107 - 0
src/pyCBD/tracers/tracerVCD.py

@@ -0,0 +1,107 @@
+"""
+VCD tracer for the CBD Simulator.
+
+Hint:
+	VCD files can be read by gtkWave (for instance).
+"""
+
+from .baseTracer import BaseTracer
+from pyCBD.util import PYCBD_VERSION
+import re, math
+
+_PRECS = ["s", "ms", "us", "ps", "fs"]
+
+
+class VCDTracer(BaseTracer):
+	"""
+	VCD tracer for the CBD Simulator.
+
+	Note:
+		During the simulation, all intermediary results will be constantly
+		written to the file.
+
+	See Also:
+		- `Value Change Dump <https://en.wikipedia.org/wiki/Value_change_dump>`_
+		- `Verilog Hardware Description Language Reference <https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=954909&tag=1>`_
+	"""
+
+	def __init__(self, uid=-1, filename=None):
+		super().__init__(uid, filename)
+		self.__mapper = {}
+		self.__map_range = 33, 126
+		self.__prec = 3
+		self.__assign_id = self.__map_range[0]
+
+	def _groups(self, names, root=""):
+		out = {root: {}}
+		for name in names:
+			vals = name.split(".")
+			if len(vals) == 1:
+				out[root][vals[0]] = {}
+			else:
+				cur = out[root]
+				for v in vals:
+					cur.setdefault(v, {})
+					cur = cur[v]
+		return out
+
+	def traceVars(self, groups, prefix=""):
+		for key in sorted(groups.keys()):
+			if len(groups[key]) == 0:
+				self.__mapper[prefix + key] = chr(self.__assign_id)
+				self.traceln("$var real 32 %s %s $end" % (chr(self.__assign_id), re.sub('[^0-9a-zA-Z]+', "_", key)))
+				self.__assign_id += 1
+				if self.__assign_id > self.__map_range[1]:
+					raise ValueError("Too many outputs to map for VCD!")
+			else:
+				self.traceln("$scope module %s $end" % re.sub('[^0-9a-zA-Z]+', "_", key))
+				self.traceVars(groups[key], prefix + key + ".")
+				self.traceln("$upscope $end")
+
+
+	def startTracer(self, recover=False):
+		super().startTracer(recover)
+		if self._interpolator.is_ci_set():
+			self.__prec = self._interpolator.get_precision()
+
+		self.traceln("$date\n\tGenerated at %s.\n$end\n$version\n\tGenerated by pyCBD %s.\n$end" % (self.timeInfo(), PYCBD_VERSION))
+		self.traceln("$timescale 1%s $end" % _PRECS[self.__prec // 3])
+
+		header = self._groups(self._interpolator.get_header())[""]
+		self.traceln("$scope module globals $end")
+		self.traceVars({"time": {}})
+		self.traceln("$upscope $end")
+		self.traceln("$scope module %s $end" % self._model_name)
+		self.traceVars(header)
+		self.traceln("$upscope $end")
+		self.traceln("$enddefinitions $end")
+
+	def traceEndNewIteration(self, curIt, time):
+		# TODO: somehow, the interpolated values of the clock are wrong -- check this!
+		if self._interpolator.is_ci_set():
+			cnt = self._interpolator.get_deltas_passed(time)
+			if cnt > 0:
+				if curIt > 0:
+					self.traceln("#%d" % round(time * (10 ** self.__prec)))
+				self.traceln("$dumpvars")
+			for i in range(cnt):
+				x = self._interpolator.get_next_computation_point()
+				if x <= time:
+					vals = self._interpolator.compute(x)
+					self.traceln("r%.6f %s" % (x, self.__mapper["time"]))
+					for ix, h in enumerate(self._interpolator.get_header()):
+						self.traceln("r%.6f %s" % (vals[ix], self.__mapper[h]))
+				self._interpolator.update_time()
+			self._interpolator.post_compute()
+			if cnt > 0:
+				self.traceln("$end")
+		else:
+			# FIXME: what when multi-rate simulation?
+			if curIt > 0:
+				self.traceln("#%d" % round(time * (10 ** self.__prec)))
+			self.traceln("$dumpvars")
+			self.traceln("r%.6f %s" % (time, self.__mapper["time"]))
+			for sig in self._interpolator.get_header():
+				self.traceln("r%.6f %s" % (self._interpolator.get_curr_signal(sig)[1], self.__mapper[sig]))
+			self.traceln("$end")
+

+ 1 - 1
src/pyCBD/tracers/tracerVerbose.py

@@ -9,7 +9,7 @@ class VerboseTracer(BaseTracer):
 	"""
 	Verbose tracer for the CBD Simulator.
 	"""
-	def traceNewIteration(self, curIt, time):
+	def traceStartNewIteration(self, curIt, time):
 		txt1 = COLOR.colorize("Iteration: ", COLOR.BOLD)
 		txt2 = COLOR.colorize("{:>5}".format(curIt), COLOR.GREEN)
 		txt3 = COLOR.colorize("; Time: ", COLOR.BOLD)

+ 3 - 0
src/pyCBD/util.py

@@ -1,5 +1,8 @@
 import sys
 
+PYCBD_VERSION = '1.6'
+"""The version of the CBD simulator."""
+
 PYTHON_VERSION = sys.version_info[0]
 """The python version of the simulation."""
 

+ 1 - 1
src/setup.py

@@ -1,7 +1,7 @@
 from setuptools import setup, find_packages
 
 setup(name="pyCBD",
-      version="1.5",
+      version="1.6",
       description="Python CBD simulator",
       author=", ".join([
 	      "Marc Provost <Marc.Provost@mail.mcgill.ca>",