#
# $Id: main.py,v 1.1.1.1 2003/03/22 05:15:49 dbelan2 Exp $
#
# Main program
#
# Based on pseudo-code given in class.
#
# $Log: main.py,v $
# Revision 1.1.1.1  2003/03/22 05:15:49  dbelan2
# Initial import of all public_html on www.cs.mcgill.ca.
#
# Revision 1.30  2001/10/01 21:44:02  hchen19
# ent_t to end_t  typing error
#
# Revision 1.28  2001/10/01 21:27:32  hchen19
# Find optimal k automatically.
#
# Revision 1.27  2001/10/01 16:27:19  hchen19
# no comment
#
# Revision 1.26  2001/10/01 16:11:26  dbelan2
# Move some Plotter functionnality to gui module.
#
# Revision 1.25  2001/10/01 15:36:52  hchen19
# bug fixed in saveState() and loadState()
#
# Revision 1.24  2001/10/01 03:07:27  hchen19
# bug fixed in output_time_and_state_var()
#
# Revision 1.23  2001/09/29 23:53:58  hchen19
# a bug fixed in changeBlockValue() method.
#
# Revision 1.21  2001/09/29 21:41:31  hchen19
# One bug fixed in plot method.
#
# Revision 1.20  2001/09/28 23:50:35  hchen19
# system structure changed. System save the running result for each block plus the
# time stamp. In this way, use can plot on different blocks without running the
# simulator again. If user already specified the plotting blocks, the system will
# plot the result after the simulation. The same thing is with saving result to file
#
# Revision 1.19  2001/09/28 21:25:08  hchen19
# system can also store time besides the block output in a user specified file
#
# Revision 1.18  2001/09/28 20:56:54  hchen19
# system can re-plot on the same window, the previous plot will be erased
#
# Revision 1.17  2001/09/28 19:57:43  hchen19
# system can re-run using the initial configuration of the model
#
# Revision 1.16  2001/09/28 15:31:23  hchen19
# the system can save the simulation result to a specific file
#
# Revision 1.15  2001/09/26 20:32:46  dbelan2
# *** empty log message ***
#
# Revision 1.14  2001/09/25 16:54:46  dbelan2
# - Basic save state, load state implemented.
#
# Revision 1.13  2001/09/25 03:12:37  dbelan2
# - Now can plot several lines on the same plot.
#   Still need to add code to pick a different colour each
#   time.
#
# Revision 1.12  2001/09/25 02:36:28  dbelan2
# - Implemented methods to specify output format and
#   output device (plot or file).  Can also specify
#   what variable (including time) to plot.
#
# Revision 1.11  2001/09/25 01:13:42  dbelan2
# - Improved code structure by making it more object
#   oriented.
# - Basic command interpreter running
# - Getting more general
#
# Revision 1.10  2001/09/23 23:32:57  dbelan2
# - Code to remove all Relays added before graph constructed.
# - Simulator core updated to use sorted order and compute
#   constants once.
#
# Revision 1.9  2001/09/22 02:04:34  dbelan2
# - now does block sorting
# - implemented printModelInfo()
#
# Revision 1.8  2001/09/22 00:30:19  dbelan2
# Bug fix - Graph simplifier - Int was not removed
# from adj list.
# Now cycle detection seems to work correctly on simplified
# graph.
#
# Revision 1.7  2001/09/21 16:51:27  dbelan2
# Reversing graph edges and post ordering sorting done.
#
# Revision 1.6  2001/09/21 03:02:23  dbelan2
# Added code to simplify graph (removes constants and integrators).
#
# Revision 1.5  2001/09/20 21:04:20  dbelan2
# Fixed bug -- edge direction in dep graph.
#
# Revision 1.4  2001/09/20 19:49:58  dbelan2
# Added code to plot circle.  Circle works but plotting code options
# is still hard coded.
#
# Revision 1.3  2001/09/20 18:14:09  dbelan2
# Basic simulator running.  No cycle test.  Output is updated.
# But still need to test if output OK.
#
# Revision 1.2  2001/09/20 17:32:35  dbelan2
# Main structure implemented.
#
# Revision 1.1  2001/09/16 22:01:15  dbelan2
# Basic structure in code and pseudocode.
#
#


import sys
from data import *
from alg import *
from Plotter import *

import pickle

#plot it!
execfile('plot.py')

DEFAULTS_FILE = 'defaults.solver'


class Solver:

    # attributes are:
    # model   - a Model object
    # system  - synonym to model.blocks
    # graph   - dependency graph
    #
    # intblocks
    # nonintblockssorted
    # constantblocks
    #
    # step_size
    # comm_int
    # start_t
    # end_t
    #
    def __init__(self, model = None):
        self.model = model
        
        self.output_time = 0.0

        self.resultfilename=None
        self.outputblocknames=None
        self.outputdata=[]

        self.totaloutputdata=[]

        self.plotready = 0
        self.plotblocknames=None
        self.plotpoints=[]
        self.allLines = []
        self.name2block={}

        self.blocknameindexdictionary={}

        self.setDefaults()

       # self.plotter=Plotter()

        if model != None:
            self.computeModel()

    def computeModel(self):        
        self.system = self.model.blocks
        self.removeRelays()

        for block in self.model.blocks:
            self.name2block[block.name]=block
            print block.name

        self.blocknameindexdictionary['time']=0
        i=1
        for block in self.model.blocks:
            self.blocknameindexdictionary[block.name]=i
            i=i+1
            
        self.buildDepGraph()
        self.simplifyGraph()
        if self.isCyclic():
            print "Algebraic Loop, cannot proceed"
            sys.exit(1)
        else:
            print "No Loops, proceeding..."
        self.sort()


        #self.printModelInfo()  # temp - for debugging

    def blockInModelCheck(self, blockname):
        i=0
        for block in self.model.blocks:
            if blockname==block.name:
                i=1
                break
        return i

    def changeBlockValue(self, blockname, value):
        for block in self.model.blocks:
            if block.name==blockname:
                block.value=value
                break
    
    def setOutputFormat(self, blocknames):
        self.outputblocknames=blocknames
                    
    def setXYOutput(self, xname, yname):
        self.plotblocknames=[]
        self.plotblocknames.append(xname)
        self.plotblocknames.append(yname)

    def getvars(self):
        return [self.constantblocks, \
                self.intblocks, \
                self.nonintblockssorted]
                
    def getvar(self, varname):
        pass
                            

    def go(self):
        self.sim_loop()

        
    def setDefaults(self):
        # system defaults
        self.step_size = 0.1
        self.comm_int  = 0
        self.start_t   = 0
        self.end_t     = 0
        self.default_output = 0

        # overrides with saved defaults if a defautls
        # file exits

        try:
            execfile(DEFAULTS_FILE)
        except IOError:
            print "Error opening defaults file"
            print "Using system defaults"

        if self.model != None:
            self.system = self.model.blocks
            for block in self.system:
                if isinstance(block, Integrator):
                    block.output_value = block.getIC()
                else:
                    block.output_value = 0

    def reInitializeSystem(self):
        for block in self.system:
            if isinstance(block, Integrator):
                block.output_value = block.getIC()
            else:
                block.output_value = 0
                    

    def writeDefaults(self):
        file = open(DEFAULTS_FILE, "w")
        file.write('self.step_size = '+`self.step_size`+'\n')
        file.write('self.comm_int = '+`self.comm_int`+'\n')
        file.write('self.start_t = '+`self.start_t`+'\n')
        file.write('self.end_t = '+`self.end_t`+'\n')
        file.close()

    def saveState(self, filename):
        f = open(filename, "w")

        # model structure
        pickle.dump(self.model, f)
        pickle.dump(self.graph, f)
        pickle.dump(self.constantblocks, f)
        pickle.dump(self.intblocks, f)
        pickle.dump(self.nonintblockssorted, f)

        # solver parameters
        pickle.dump(self.step_size, f)
        pickle.dump(self.comm_int, f)
        pickle.dump(self.start_t, f)
        pickle.dump(self.end_t, f)

        # output settings
        pickle.dump(self.outputblocknames, f)
        pickle.dump(self.plotblocknames, f)

        f.close()

    def loadState(self, filename):
        f = open(filename, "r")

        # model structure
        self.model = pickle.load(f)
        self.graph = pickle.load(f)
        self.constantblocks = pickle.load(f)
        self.intblocks = pickle.load(f)
        self.nonintblockssorted = pickle.load(f)

        # solver parameters
        self.step_size = pickle.load(f)
        self.comm_int = pickle.load(f)
        self.start_t = pickle.load(f) 
        self.end_t = pickle.load(f)

        # output settings
        self.outputblocknames=pickle.load(f)
        self.plotblocknames=pickle.load(f)

        f.close()

        self.computeModel()


    def saveResultToFile(self):
        if self.resultfilename!=None and self.outputblocknames!=None:
            f=open(self.resultfilename, "w")
            for j in range(len(self.outputblocknames)):
                f.write(str(self.outputblocknames[j])+'\t')
            f.write('\n')
            for eachstepdata in self.totaloutputdata:
                for eachblockname in self.outputblocknames:
                    index=self.blocknameindexdictionary[eachblockname]
                    f.write(str(eachstepdata[index])+'\t')
                f.write('\n')
            f.close()
        else:
            pass

    # Remove all Relays

    # to do: add following simplification
    # if block is Relay and only 1 output block
    #   link relay input block output to the Relay output block input
    #  and remove relay.    <== this is not quite right
    #
    # Actually it looks that objects (Integrator, Adder, ...)
    # can have more than 1 output link (it's like relay inside)


    # So all Relay objs can be removed as long as inputs/outputs
    # properly connected.
    #
    # A relay R has a single input I and can have several outputs
    # O1, O2, ..., On.
    #
    # alg:
    # for each output blocks Bi do
    #     Bi.inputs.remove(R)
    #     Bi.inputs.add(I)
    # I.outputs.add(Bi)
    # I.outputs.remove(R)
    #

    #
    #
    #
    # Note1: only the dependency graph (formed by Vertex(cies)) changes.
    # The block graph induced by inputs/outputs lists is not changed.
    # Note2: relays have an order.  Cannot remove them in graph.v
    # without also removing the underlying block object and its I/O links.
    #
    # 
    #
    #

    def removeRelays(self):
        for block in self.system[:]:
            if isinstance(block, Relay):
                block.inputs[0].outputs.remove(block)
                for output in block.outputs:
                    block.inputs[0].outputs.append(output)
                    output.inputs.remove(block)
                    output.inputs.append(block.inputs[0])
                self.system.remove(block)


    # Read system network (graph structure)

    # dependency graph built
    # a block B depends on all input blocks
    def buildDepGraph(self):
        vertices = []
        for block in self.system:
            vertices.append(Vertex(obj = block))
        # adj list
        for v in vertices:
            for input in v.obj.inputs:
                v.adj.append(input.v)
        self.graph = Graph(vertices)
        print "Dependency graph"
        print self.graph


    # simplify graph
    # ex: remove constants because there value is known
    # and does not change.
    # Also Integrator because have an output at the beginning
    
    # Note: the network structure of the block remains untouch
    
    # iterating over a copy because v.adj list is modified in loop

    def simplifyGraph(self):

        for v in self.graph.V[:]:
            print "v is", v
            if isinstance(v.obj, Constant) or isinstance(v.obj, Integrator):
                self.graph.V.remove(v)
                print "->", v, "removed from graph.V"
            else:
                for u in v.adj[:]:
                    print "  u is", u
                    if isinstance(u.obj, Constant) or isinstance(u.obj, Integrator):
                        v.adj.remove(u)
                        print "  ->", u, "removed from v.adj"
      
        print "Simplified Dependency Graph"
        print self.graph

        
        
    #Detect algebraic loops
    def isCyclic(self):
        return self.graph.detectLoops()


    # Sort the non-integrator blocks

    # note: constants are also not part of this.
    # There value is set once and does not change.

    def sort(self):

        
        self.nonintblockssorted = []
        self.intblocks = []
        self.constantblocks = []

        # sort vertices in dep graph
        verticessorted = self.graph.topologicalSort()

        # put non-Integrator blocks in order obtained
        for v in verticessorted:
            self.nonintblockssorted.append(v.obj)

        # fill in other lists of blocks
        for block in self.system:
            if isinstance(block, Integrator):
                self.intblocks.append(block)
            elif isinstance(block, Constant):
                self.constantblocks.append(block)


    def printModelInfo(self):
        print "\n\nModel Graph Info"
        print "Dep Graph"
        print self.graph
        print "These constants will be computed once:"
        print map(str, self.constantblocks)
        print "The integrators are:"
        print map(str, self.intblocks)
        print "Finally, the SORTED non-integrator, non-constant are:"
        for block in self.nonintblockssorted:
            print block, "(",
            for dep in block.v.adj:
                print dep,
            print ")"
        print "Dependencies are in brackets"

        print
        
        print "step_size is", self.step_size
        print "comm_int  is", self.comm_int
        print "start_t   is", self.start_t
        print "end_t     is", self.end_t


    # Simulation Kernel Loop
    def sim_loop(self):
        #print "in sim_loop()"

        self.reInitializeSystem()
            
        self.points = []
        self.outputdata=[]
        self.totaloutputdata=[]
        current_time = 0
        self.output_time=0

        # Constants needs only to be computed once
        for block in self.constantblocks:
            block.calcOutput()

        while not self.end_of_simulation(current_time):
            self.update_blocks(current_time)
            self.output_time_and_state_var(current_time)
            current_time = current_time + self.step_size

        self.saveResultToFile()
        #self.plot()  will let gui take care of this

    def update_blocks(self, current_time):
        # print "In Update_blocks"

        # update Integrator blocks (order does not matter)
        for block in self.intblocks:
            block.calcOutput(self.step_size)

        # now update non-Integrator blocks in "sorted" order
        for block in self.nonintblockssorted:
            block.calcOutput()


    def end_of_simulation(self, current_time):
        #global current_timend_time
        #print "In End_of_simulation()"
        if current_time >= self.end_t:
            return 1
        else:
            return 0
    #condition(state_values) == TRUE


    def output_time_and_state_var(self, current_time):

        if (self.output_time <= current_time):
            #self.output_time = self.output_time + self.comm_int

            temp=[]
            temp.append(self.output_time)
            for block in self.model.blocks:
                 temp.append(block.output_value)
            self.totaloutputdata.append(temp)
            self.output_time = self.output_time + self.comm_int 

    #def plot(self):
    def getPoints(self):
        if self.plotblocknames!=None:
            x=self.blocknameindexdictionary[self.plotblocknames[0]]
            y=self.blocknameindexdictionary[self.plotblocknames[1]]
            print `x`+' vice '+`y`

            self.plotpoints=[]

            for eachstepdata in self.totaloutputdata:
                self.plotpoints.append((eachstepdata[x], eachstepdata[y]))
                print `eachstepdata[x]`+'   '+`eachstepdata[y]`
        
##             if not self.plotready:
##                 self.plotter=Plotter()
##                 self.plotready=1
            
            #self.plotter.plot(self.plotpoints)
            return self.plotpoints

        else:
            pass
        


















