### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###
##                    308-522 -- Modelling and Simulation
##                                  Fall 2002
##                             --- ASSIGNMENT 1 ---
##
## SIM_startResume.py
##
## last modified: 09/30/02
### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###

import threading
## MODIFIED: those were only needed for the example.
# from time import sleep
# from math import sin, cos, pi
from Graph import *  # Topological sorting and loop detection
from math import *  # Support for generic blocks

### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###

# "MODE" determines the action:
#       1 -- Normal simulation mode
#       0 -- Special mode: testing the ballistic problem
MODE = 1


### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###

if MODE:  # Normal simulation mode
  ## MODIFIED: renamed class "Generator" to "TS_Simulator"
  class TS_Simulator:
    """ Time-Slicing Simulator.

    MODIFIED: method "addDataItems" is removed. New methods are:
          "initSimul" -- Initialize the time-slicing simulator,
          "mainLoop"  -- Main simulation loop.
          "processDelayBlocks"
          "processConnectors"
          "sendOutput"
    The two first methods correspond to the "timeSlicing" pseudo-code algorithm
    in the document "Causal Block Diagram Algorithms". The other methods have
    self-explanatory names.
    """
    def __init__(self, model):
      """ MODIFIED: the constructor now takes a model as an argument, instead
      of a DataSet object.
      """
      self.running = 0  # generate data iff true
      self.model = model
      self.dataSet = model.data  # DataSet object to which data-items are added

    def initSimul(self):
      """ Initialize the time-slicing simulator: Topological sorting of the
      connectors, and detection of algebraic loops.
      """
      # List of all blocks:
      blocks = self.model.listNodes["Block"]
      # There is currently no support for Delay and Derivative blocks. Model is
      # considered invalid if it includes one of them.
      # NOTE that throughout this code we use
      #       B.block_type.getValue()[1]
      # to know the type of a block "B". The value we get indicates:
      #       0 -- Generic     5 -- Delay
      #       1 -- Negator     6 -- Integrator
      #       2 -- Inverter    7 -- Derivative
      #       3 -- Adder       8 -- Constant
      #       4 -- Product     9 -- Time
      if reduce(lambda u, v : u or v.block_type.getValue()[1] in (5, 7),      \
            blocks, 0):
        raise NotImplementedError, "unsupported blocks"

      # Remember the list of integrator blocks:
      self.delayBlocks = filter(lambda x : x.block_type.getValue()[1] == 6,   \
            blocks)
      # Remember the list of n-ary operator blocks (Adder and Product). Those
      # will need to be initialized before each simulation step.
      self.naryBlocks = filter(lambda x : x.block_type.getValue()[1] in       \
            (3, 4), blocks)

      # Remember the time block:
      self.timeBlock = filter(lambda x : x.block_type.getValue()[1] == 9,     \
            blocks)[0]

      # Remember the list of all connections to the plotter:
      self.plotNodes = self.model.listNodes["PlotConnection"]

      # TOPOLOGICAL SORT:
      # "topSort" sorts the connectors in topological order. The function
      # ignores (sort of -- see details in the code) integrator blocks.
      # The returned list is the sorted dependency graph.
      self.topsort = topSort( self.model.listNodes["BlockConnection"] )

      # IDENTIFY STRONG COMPONENTS:
      # Note that "strongComp" also handles integrator blocks in a special
      # manner (see details in the code).
      self.strongcomp = strongComp( self.topsort )
      for item in self.strongcomp:
        if len(item) > 1:
          # For the moment, avoid algebraic loops...
          raise NotImplementedError, "algebraic loop detected"

      # GET MODEL'S ATTRIBUTES:
      # TODO: verify the values are legitimate
      self.delta     = self.model.simul_delta_t.getValue()
      self.deltaComm = self.model.simul_comm_interval.getValue()
      self.initTime = self.model.simul_t_init.getValue()
      self.finalTime = self.model.simul_t_final.getValue()

      # Current simulation time
      self.currTime = self.initTime
      # Next communication time:
      self.nextComm = self.currTime + self.deltaComm

      # Initialize connectors:
      self.processConnectors()

      # Print initial conditions:
      self.sendOutput()

      # Advance the time:
      self.currTime += self.delta

      # START THE ACTUAL SIMULATION:
      self.mainLoop()

    def mainLoop(self):
      """ Main simulation loop. """

      self.running = 1

      # MAIN LOOP:
      while self.currTime <= self.finalTime and self.running:

        # Process delay blocks:
        self.processDelayBlocks()

        # Initialize connectors:
        self.processConnectors()

        # Plot if required
        if self.currTime >= self.nextComm:
          self.sendOutput()
          self.nextComm = self.currTime + self.deltaComm

        # Advance the time:
        self.currTime += self.delta


    def processDelayBlocks(self):
      """ Process all delay blocks (integrators).

      This is the Euler-Cauchy approximation:
            x_1 = x_0 + h * f(x_0)
      with:
            x_0    -- the current state  (initial "block_out_value" value)
            x_1    -- the next state  (updated "block_out_value" value)
            h      -- the time step  ("self.delta")
            f(x_0) -- the current slope  ("block_tmp_value" value)
      """
      for block in self.delayBlocks:
        block.block_out_value.setValue( block.block_out_value.getValue() +    \
              self.delta * block.block_tmp_value.getValue() )


    def processConnectors(self):
      """ Process all connectors in "topsort" order.

      Note that the integrator blocks' IC port is used only at initialisation,
      i.e. when "self.currTime == self.initTime".
      """
      # Reset n-ary operator blocks: the identity operator for the supported
      # operation (0.0 for addition, 1.0 for multiplication) is placed in the
      # blocks' "block_out_value" attribute.
      for block in self.naryBlocks:
        block.block_out_value.setValue( block.block_type.getValue()[1] == 4 )

      # Process in "topsort" order:
      for node in self.topsort:
        # Get the value from the source block's "block_out_value" attribute:
        A = node.in_connections_[0]  # only one block as a source
        # The Time blocks' output value is just the block's initial value
        # plus the elapsed time:
        if A.block_type.getValue()[1] == 9:
          value = A.block_out_value.getValue() +                              \
                (self.currTime - self.initTime)
        else:
          value = A.block_out_value.getValue()

        # Push the value into the destination block:
        B = node.out_connections_[0]  # only one block as a destination
        Btype = B.block_type.getValue()[1]

        if Btype == 0:  # Generic Block
          # Generic blocks are unary functions. The function implemented is
          # stored as a string in the block's "block_operator" attribute
          # (e.g., 'sin', 'cos'...). For now, we support only those functions
          # where "fnc(x)" is meaningful (where "block_operator == 'fnc'",
          # and "x" is a float).
          fnc = B.block_operator.getValue()
          B.block_out_value.setValue( eval(fnc + "(%f)" % (value)) )

        if Btype == 1:  # Negator Block
          B.block_out_value.setValue( -value )

        if Btype == 2:  # Inverter Block
          B.block_out_value.setValue( 1.0/value )

        if Btype == 3:  # Adder Block
          B.block_out_value.setValue( B.block_out_value.getValue() + value )

        if Btype == 4:  # Product Block
          B.block_out_value.setValue( B.block_out_value.getValue() * value )

        if Btype == 5:  # Delay Block
          raise NotImplementedError, "delay block?!"

        if Btype == 6:  # Integrator Block
          # The connection is processed differently whether it corresponds to
          # the integrator's IC port or regular input port: at initialisation
          # _only_, the value on the IC port is copied into the integrator's
          # "block_out_value" attribute. Value from other ports is copied into
          # the integrator's "block_tmp_value" attribute (at any time).
          if node == B.block_IC_port[0]:
            if self.currTime == self.initTime:
              B.block_out_value.setValue( value )
          else:
            B.block_tmp_value.setValue( value )

        if Btype == 7:  # Derivative Block
          raise NotImplementedError, "derivative block?!"

        if Btype == 8:  # Constant Block
          raise SyntaxError, "connector into a constant block"

        if Btype == 9:  # Time Block
          raise SyntaxError, "connector into a time block"


    def sendOutput(self):
      """ Send outputs to plotter, and update clock.
      """
      dict = {}
      for node in self.plotNodes:
        # Get the value from the source block's "block_out_value" attribute:
        A = node.in_connections_[0]  # only one block as a source
        # The Time blocks' output value is just the block's initial value
        # plus the elapsed time:
        if A.block_type.getValue()[1] == 9:
          value = A.block_out_value.getValue() +                              \
                (self.currTime - self.initTime)
        else:
          value = A.block_out_value.getValue()

        # Get the node's name (make sure it exists):
        name = A.block_name.getValue()
        if name == '':
          raise SyntaxError, "output name mising"

        dict[name] = value

      self.sendPlotter(dict)

      # Update time block:
      self.timeBlock.graphObject_.ModifyAttribute("block_out_value", self.currTime)



    def stop(self):
      self.running = 0


    def sendPlotter(self, dict):
      """ The keys in the dictionnary "dict" are matched with the entries in
      "self.dataSet.titles" to know what entry correspond to what index in the
      data-item.
      """
      m = self.dataSet.titles  # ordered titles
      temp = [None]*len(m)
      for k in dict:
        try:
          i = m.index(k)
        except ValueError:
          print "***", k
          # pass  # THIS SHOULDN'T HAPPEND
        temp[i] = dict[k]

      # data-item ordered : add it to dataSet
      # (this will trigger updating the appropriate trajectory
      # plots in the GUI)
      self.dataSet.append( temp )


### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###


if not MODE:  # Special mode: testing the ballistic problem
# This is essentially the same code as above. Modifications are
# indicated by BALLISTIC.
  ## MODIFIED: renamed class "Generator" to "TS_Simulator"
  class TS_Simulator:
    """ Time-Slicing Simulator.

    MODIFIED: method "addDataItems" is removed. New methods are:
          "initSimul" -- Initialize the time-slicing simulator,
          "mainLoop"  -- Main simulation loop.
          "processDelayBlocks"
          "processConnectors"
          "sendOutput"
    The two first methods correspond to the "timeSlicing" pseudo-code algorithm
    in the document "Causal Block Diagram Algorithms". The other methods have
    self-explanatory names.
    """
    def __init__(self, model):
      """ MODIFIED: the constructor now takes a model as an argument, instead
      of a DataSet object.
      """
      self.running = 0  # generate data iff true
      self.model = model
      self.dataSet = model.data  # DataSet object to which data-items are added

    def initSimul(self):
      """ Initialize the time-slicing simulator: Topological sorting of the
      connectors, and detection of algebraic loops.
      """
      # List of all blocks:
      blocks = self.model.listNodes["Block"]
      # There is currently no support for Delay and Derivative blocks. Model is
      # considered invalid if it includes one of them.
      # NOTE that throughout this code we use
      #       B.block_type.getValue()[1]
      # to know the type of a block "B". The value we get indicates:
      #       0 -- Generic     5 -- Delay
      #       1 -- Negator     6 -- Integrator
      #       2 -- Inverter    7 -- Derivative
      #       3 -- Adder       8 -- Constant
      #       4 -- Product     9 -- Time
      if reduce(lambda u, v : u or v.block_type.getValue()[1] in (5, 7),      \
            blocks, 0):
        raise NotImplementedError, "unsupported blocks"

      # Remember the list of integrator blocks:
      self.delayBlocks = filter(lambda x : x.block_type.getValue()[1] == 6,   \
            blocks)
      # Remember the list of n-ary operator blocks (Adder and Product). Those
      # will need to be initialized before each simulation step.
      self.naryBlocks = filter(lambda x : x.block_type.getValue()[1] in       \
            (3, 4), blocks)

      # Remember the list of all connections to the plotter:
      self.plotNodes = self.model.listNodes["PlotConnection"]

      # TOPOLOGICAL SORT:
      # "topSort" sorts the connectors in topological order. The function
      # ignores (sort of -- see details in the code) integrator blocks.
      # The returned list is the sorted dependency graph.
      self.topsort = topSort( self.model.listNodes["BlockConnection"] )

      # IDENTIFY STRONG COMPONENTS:
      # Note that "strongComp" also handles integrator blocks in a special
      # manner (see details in the code).
      self.strongcomp = strongComp( self.topsort )
      for item in self.strongcomp:
        if len(item) > 1:
          # For the moment, avoid algebraic loops...
          raise NotImplementedError, "algebraic loop detected"

      # GET MODEL'S ATTRIBUTES:
      # TODO: verify the values are legitimate
      self.delta     = self.model.simul_delta_t.getValue()
      self.deltaComm = self.model.simul_comm_interval.getValue()
      self.initTime = self.model.simul_t_init.getValue()
      self.finalTime = self.model.simul_t_final.getValue()

      # BALLISTIC: Identify three relevant blocks:
      try:
        # Block whose name is "\theta":
        self.thetaBlock = filter(lambda x : x.block_name.getValue() ==        \
              "\\theta", blocks)[0]
        # Block whose name is "x":
        self.XBlock = filter(lambda x : x.block_name.getValue() ==            \
              "x", blocks)[0]
        # Block whose name is "y":
        self.YBlock = filter(lambda x : x.block_name.getValue() ==            \
              "y", blocks)[0]
      except IndexError:
        raise SyntaxError, "couldn't find expected block"

      # BALLISTIC: lines below moved to "self.mainLoop":
      # Current simulation time
      #self.currTime = self.initTime
      # Next communication time:
      #self.nextComm = self.currTime + self.deltaComm

      # Initialize connectors:
      #self.processConnectors()

      # Print initial conditions:
      #self.sendOutput()

      # Advance the time:
      #self.currTime += self.delta

      # START THE ACTUAL SIMULATION:
      self.mainLoop()


    def mainLoop(self):
      """ Main simulation loop. """

      self.running = 1

      theta = 0.0  # Initial theta value (in radians)
      samples = 90  # number of sample angles (between 0 and pi/4)

      while theta <= pi/2.0 and self.running:

        # Modify "\theta" block:
        self.thetaBlock.block_out_value.setValue(theta)

        # INITIALIZE SIMULATION:
        # Current simulation time
        self.currTime = self.initTime
        # Next communication time:
        self.nextComm = self.currTime + self.deltaComm

        # Initialize connectors:
        self.processConnectors()

        # Print initial conditions:
        #self.sendOutput()

        # Advance the time:
        self.currTime += self.delta

        # MAIN SIMULATION LOOP:
        # Termination condition is modified: simulation terminates when
        # the block named "y" has a state <= 1.0
        while self.running:

          # Process delay blocks:
          self.processDelayBlocks()

          # Initialize connectors:
          self.processConnectors()

          # TERMINATION CONDITION
          if self.YBlock.block_out_value.getValue() <= 1.0:
            break

          # BALLISTIC: plotting scheme is changed
          # Plot if required
          #if self.currTime >= self.nextComm:
          #  self.sendOutput()
          #  self.nextComm = self.currTime + self.deltaComm

          # Advance the time:
          self.currTime += self.delta

        # PLOTTING:
        # We plot the current time, and the distance of the projectile in
        # function of the angle theta. The plotter has already three fields (t,
        # x and y). By using the method "sendPlotter" rather than "sendOutput",
        # we associate whatever data we wish to those fields. In our case:
        #       "t" -- angle theta (in degrees)
        #       "x" -- distance of the projectile (from target)
        #       "y" -- current time (times 6)

        ## CONVERT THETA TO DEGREES
        dict = { "t": theta*180./pi,
                 "x": abs(self.XBlock.block_out_value.getValue() - 30.0),
                 "y": self.currTime * 6.}
        self.sendPlotter( dict )

        # Update theta:
        theta += pi/(2.0*samples)


    def processDelayBlocks(self):
      """ Process all delay blocks (integrators).

      This is the Euler-Cauchy approximation:
            x_1 = x_0 + h * f(x_0)
      with:
            x_0    -- the current state  (initial "block_out_value" value)
            x_1    -- the next state  (updated "block_out_value" value)
            h      -- the time step  ("self.delta")
            f(x_0) -- the current slope  ("block_tmp_value" value)
      """
      for block in self.delayBlocks:
        block.block_out_value.setValue( block.block_out_value.getValue() +    \
              self.delta * block.block_tmp_value.getValue() )


    def processConnectors(self):
      """ Process all connectors in "topsort" order.

      Note that the integrator blocks' IC port is used only at initialisation,
      i.e. when "self.currTime == self.initTime".
      """
      # Reset n-ary operator blocks: the identity operator for the supported
      # operation (0.0 for addition, 1.0 for multiplication) is placed in the
      # blocks' "block_out_value" attribute.
      for block in self.naryBlocks:
        block.block_out_value.setValue( block.block_type.getValue()[1] == 4 )

      # Process in "topsort" order:
      for node in self.topsort:
        # Get the value from the source block's "block_out_value" attribute:
        A = node.in_connections_[0]  # only one block as a source
        # The Time blocks' output value is just the block's initial value
        # plus the elapsed time:
        if A.block_type.getValue()[1] == 9:
          value = A.block_out_value.getValue() +                              \
                (self.currTime - self.initTime)
        else:
          value = A.block_out_value.getValue()

        # Push the value into the destination block:
        B = node.out_connections_[0]  # only one block as a destination
        Btype = B.block_type.getValue()[1]

        if Btype == 0:  # Generic Block
          # Generic blocks are unary functions. The function implemented is
          # stored as a string in the block's "block_operator" attribute
          # (e.g., 'sin', 'cos'...). For now, we support only those functions
          # where "fnc(x)" is meaningful (where "block_operator == 'fnc'",
          # and "x" is a float).
          fnc = B.block_operator.getValue()
          B.block_out_value.setValue( eval(fnc + "(%f)" % (value)) )

        if Btype == 1:  # Negator Block
          B.block_out_value.setValue( -value )

        if Btype == 2:  # Inverter Block
          B.block_out_value.setValue( 1.0/value )

        if Btype == 3:  # Adder Block
          B.block_out_value.setValue( B.block_out_value.getValue() + value )

        if Btype == 4:  # Product Block
          B.block_out_value.setValue( B.block_out_value.getValue() * value )

        if Btype == 5:  # Delay Block
          raise NotImplementedError, "delay block?!"

        if Btype == 6:  # Integrator Block
          # The connection is processed differently whether it corresponds to
          # the integrator's IC port or regular input port: at initialisation
          # _only_, the value on the IC port is copied into the integrator's
          # "block_out_value" attribute. Value from other ports is copied into
          # the integrator's "block_tmp_value" attribute (at any time).
          if node == B.block_IC_port[0]:
            if self.currTime == self.initTime:
              B.block_out_value.setValue( value )
          else:
            B.block_tmp_value.setValue( value )

        if Btype == 7:  # Derivative Block
          raise NotImplementedError, "derivative block?!"

        if Btype == 8:  # Constant Block
          raise SyntaxError, "connector into a constant block"

        if Btype == 9:  # Time Block
          raise SyntaxError, "connector into a time block"


    def sendOutput(self):
      """ Send outputs to plotter.
      """


      dict = {}
      for node in self.plotNodes:
        # Get the value from the source block's "block_out_value" attribute:
        A = node.in_connections_[0]  # only one block as a source
        # The Time blocks' output value is just the block's initial value
        # plus the elapsed time:
        if A.block_type.getValue()[1] == 9:
          value = A.block_out_value.getValue() +                              \
                (self.currTime - self.initTime)
        else:
          value = A.block_out_value.getValue()

        # Get the node's name (make sure it exists):
        name = A.block_name.getValue()
        if name == '':
          raise SyntaxError, "output name mising"

        dict[name] = value

      self.sendPlotter(dict)


    def stop(self):
      self.running = 0


    def sendPlotter(self, dict):
      """ The keys in the dictionnary "dict" are matched with the entries in
      "self.dataSet.titles" to know what entry correspond to what index in the
      data-item.
      """
      m = self.dataSet.titles  # ordered titles
      temp = [None]*len(m)
      for k in dict:
        try:
          i = m.index(k)
        except ValueError:
          print "***", k
          # pass  # THIS SHOULDN'T HAPPEND
        temp[i] = dict[k]

      # data-item ordered : add it to dataSet
      # (this will trigger updating the appropriate trajectory
      # plots in the GUI)
      self.dataSet.append( temp )


### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###


## MODIFIED: renamed class "GeneratorThread" to "SimulatorThread"
class SimulatorThread(threading.Thread):
  """
  MODIFIED: in this class, "generatorObject" is replaced by "simulatorObject".
  Also, added method "resume".
  """

  def __init__(self, simulatorObject):
    """ simulatorObject is a TS_Simulator instance. """

    ## MODIFIED: call base class' constructor
    #threading.Thread.__init__(self)
    self.theThread = None
    self.simulatorObject = simulatorObject

  def start(self):
    """ Start simulating. """

    # if (has been stopped) or (is finished)...
    if self.theThread == None or not(self.theThread.isAlive()):
      # Empty the DataSet
      self.simulatorObject.dataSet.clear()
      # Create the thread
      ## MODIFIED: "addDataItems" replaced with "initSimul"
      self.theThread = \
            threading.Thread(target=self.simulatorObject.initSimul)
      # Start the thread
      self.theThread.start()

  def resume(self):
    """ Resume simulation. """
    self.theThread = \
          threading.Thread(target=self.simulatorObject.mainLoop)
    # Start the thread
    self.theThread.start()


  def stop(self):
    """ Stop data-items from being generated. """

    self.simulatorObject.stop()  # stop the generator
    if self.theThread != None:
      self.theThread.join()  # wait until the thread terminates
      self.theThread = None


### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###


def startResume(parentApp, model):
 if "data" not in dir(model):
   print "\nWarning: need to set up data element (in plot window) first\n"

 else:
  # Simulation has never been run:
  if not hasattr(model, 'simulator'):
    # Code as in original "startResume"
    ## MODIFIED: "generator*" replaced with "simulator*"
    model.simulator = TS_Simulator(model)
    model.simulatorThread = SimulatorThread(model.simulator)
    model.simulatorThread.start()

  # Simulation was paused:
  elif model.simulatorThread.theThread == None and                            \
        model.simulator.running == 0:
    # Do not start from scratch (need to reset for model modifications to be
    # considered).
    model.simulatorThread.resume()

  # Simulation in progress:
  elif model.simulatorThread.theThread.isAlive():
    pass

  # Simulation terminated by itself:
  else:
    # Start from scratch (i.e., with the topological sorting): necessary in
    # case the model has been modified.
    model.simulatorThread.theThread = None  # kill thread
    model.simulator = TS_Simulator(model)
    model.simulatorThread = SimulatorThread(model.simulator)
    model.simulatorThread.start()
