# -*- coding: Latin-1 -*- """ Classes and tools for DEVS model specification """ from devsexception import DEVSException class BaseDEVS(object): """ Abstract base class for AtomicDEVS and CoupledDEVS classes. This class provides basic DEVS attributes and query/set methods. """ def __init__(self, name): """ Constructor :param name: the name of the DEVS model """ # Prevent any attempt to instantiate this abstract class if self.__class__ == BaseDEVS: raise DEVSException ("Cannot instantiate abstract class '%s' ... " % (self.__class__.__name__)) # The parent of the current model self.parent = None # The local name of the model self.name = name self.IPorts = [] self.OPorts = [] self.ports = [] # Initialise the times self.timeLast = (0.0, 0) self.timeNext = (0.0, 1) self.location = None # Variables used for optimisations self.myInput = {} self.myOutput = {} # The state queue, used for time warp self.oldStates = [] # List of all memoized states, only useful in distributed simulation # with memoization enabled self.memo = [] def modelTransition(self, state): """ DEFAULT function for Dynamic Structure DEVS, always returning False (thus indicating that no structural change is needed) :param state: a dict that can be used to save some kind of state, this object is maintained by the kernel and will be passed each time :returns: bool -- whether or not a structural change is necessary """ return False def addPort(self, name, isInput): """ Utility function to create a new port and add it everywhere where it is necessary :param name: the name of the port :param isInput: whether or not this is an input port """ name = name if name is not None else "port%s" % len(self.ports) port = Port(isInput=isInput, name=name) if isInput: self.IPorts.append(port) else: self.OPorts.append(port) port.port_id = len(self.ports) self.ports.append(port) port.hostDEVS = self return port def addInPort(self, name=None): """ Add an input port to the DEVS model. addInPort is the only proper way to add input ports to a DEVS model. As for the CoupledDEVS.addSubModel method, calls to addInPort and addOutPort can appear in any DEVS' descriptive class constructor, or the methods can be used with an instantiated object. The methods add a reference to the new port in the DEVS' IPorts attributes and set the port's hostDEVS attribute. The modeler should typically save the returned reference somewhere. :param name: the name of the port. A unique ID will be generated in case None is passed :returns: port -- the generated port """ return self.addPort(name, True) def addOutPort(self, name=None): """Add an output port to the DEVS model. addOutPort is the only proper way to add output ports to DEVS. As for the CoupledDEVS.addSubModel method, calls to addInPort and addOutPort can appear in any DEVS' descriptive class constructor, or the methods can be used with an instantiated object. The methods add a reference to the new port in the DEVS' OPorts attributes and set the port's hostDEVS attribute. The modeler should typically save the returned reference somewhere. :param name: the name of the port. A unique ID will be generated in case None is passed :returns: port -- the generated port """ return self.addPort(name, False) def getModelName(self): """ Get the local model name :returns: string -- the name of the model """ return str(self.name) def getModelFullName(self): """ Get the full model name, including the path from the root :returns: string -- the fully qualified name of the model """ return self.fullName class AtomicDEVS(BaseDEVS): """ Abstract base class for all atomic-DEVS descriptive classes. """ def __init__(self, name=None): """ Constructor for an AtomicDEVS model :param name: name of the model, can be None to have an automatically generated name """ # Prevent any attempt to instantiate this abstract class if self.__class__ == AtomicDEVS: raise DEVSException("Cannot instantiate abstract class '%s' ... " % (self.__class__.__name__)) # The minimal constructor shall first call the superclass # (i.e., BaseDEVS) constructor. BaseDEVS.__init__(self, name) self.elapsed = 0.0 self.state = None def extTransition(self, inputs): """ DEFAULT External Transition Function. Accesses state and elapsed attributes, as well as inputs through the passed dictionary. Returns the new state. .. note:: Should only write to the *state* attribute. :param inputs: dictionary containing all ports and their corresponding outputs :returns: state -- the new state of the model """ return self.state def intTransition(self): """ DEFAULT Internal Transition Function. .. note:: Should only write to the *state* attribute. :returns: state -- the new state of the model .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism. """ return self.state def confTransition(self, inputs): """ DEFAULT Confluent Transition Function. Accesses state and elapsed attributes, as well as inputs through the passed dictionary. Returns the new state. .. note:: Should only write to the *state* attribute. :param inputs: dictionary containing all ports and their corresponding outputs :returns: state -- the new state of the model """ self.state = self.intTransition() self.state = self.extTransition(inputs) return self.state def outputFnc(self): """ DEFAULT Output Function. Accesses only state attribute. Returns the output on the different ports as a dictionary. .. note:: Should **not** write to any attribute. :returns: dictionary containing output ports as keys and lists of output on that port as value .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism. """ return {} def timeAdvance(self): """ DEFAULT Time Advance Function. .. note:: Should ideally be deterministic, though this is not mandatory for simulation. :returns: the time advance of the model .. versionchanged:: 2.1 The *elapsed* attribute is no longer guaranteed to be correct as this isn't required by the DEVS formalism. """ # By default, return infinity return float('inf') def finalize(self, name, model_counter, model_ids, locations, selectHierarchy): """ Finalize the model hierarchy by doing all pre-simulation configuration .. note:: Parameters *model_ids* and *locations* are updated by reference. :param name: the name of the hierarchy above :param model_counter: the model ID counter :param model_ids: a list with all model_ids and their model :param locations: dictionary of locations and where every model runs :param selectHierarchy: hierarchy to perform selections in Classic DEVS :returns: int -- the new model ID counter """ # Give a name self.fullName = name + str(self.getModelName()) # Give a unique ID to the model itself self.model_id = model_counter self.selectHierarchy = selectHierarchy + [self] # Add the element to its designated place in the model_ids list model_ids.append(self) # Do a quick check, since this is vital to correct operation if model_ids[self.model_id] != self: raise DEVSException("Something went wrong while initializing models: IDs don't match") locations[self.location].append(self.model_id) # Return the unique ID counter, incremented so it stays unique return model_counter + 1 class CoupledDEVS(BaseDEVS): """ Abstract base class for all coupled-DEVS descriptive classes. """ def __init__(self, name=None): """ Constructor. :param name: the name of the coupled model, can be None for an automatically generated name """ # Prevent any attempt to instantiate this abstract class if self.__class__ == CoupledDEVS: raise DEVSException("Cannot instantiate abstract class '%s' ... " % (self.__class__.__name__)) # The minimal constructor shall first call the superclass # (i.e., BaseDEVS) constructor. BaseDEVS.__init__(self, name) # All components of this coupled model (the submodels) self.componentSet = [] def finalize(self, name, model_counter, model_ids, locations, selectHierarchy): """ Finalize the model hierarchy by doing all pre-simulation configuration .. note:: Parameters *model_ids* and *locations* are updated by reference. :param name: the name of the hierarchy above :param model_counter: the model ID counter :param model_ids: a list with all model_ids and their model :param locations: dictionary of locations and where every model runs :param selectHierarchy: hierarchy to perform selections in Classic DEVS :returns: int -- the new model ID counter """ # Set name, even though it will never be requested self.fullName = name + str(self.getModelName()) for i in self.componentSet: model_counter = i.finalize(self.fullName + ".", model_counter, model_ids, locations, selectHierarchy + [self]) return model_counter def addSubModel(self, model, location = None): """ Adds a specified model to the current coupled model as its child. This is the function that must be used to make distributed simulation possible. :param model: the model to be added as a child :param location: the location at which the child must run :returns: model -- the model that was created as a child .. versionchanged:: 2.1.3 model can no longer be a string, this was previously a lot more efficient in partial distribution, though this functionality was removed together with the partial distribution functionality. """ model.parent = self if location is not None: location = int(location) model.location = location if location is not None else self.location if model.location is not None and isinstance(model, CoupledDEVS): # Set the location of all children for i in model.componentSet: i.setLocation(model.location) self.componentSet.append(model) if hasattr(self, "fullName"): # Full Name is only created when starting the simulation, so we are currently in a running simulation # Dynamic Structure change self.server.getSelfProxy().dsScheduleModel(model) return model def connectPorts(self, p1, p2, z = None): """ Connects two ports together. The coupling is to begin at p1 and to end at p2. :param p1: the port at the start of the new connection :param p2: the port at the end of the new connection :param z: the translation function for the events either input-to-input, output-to-input or output-to-output. """ # For a coupling to be valid, two requirements must be met: # 1- at least one of the DEVS the ports belong to is a child of the # coupled-DEVS (i.e., self), while the other is either the # coupled-DEVS itself or another of its children. The DEVS' # 'parenthood relationship' uniquely determine the type of coupling; # 2- the types of the ports are consistent with the 'parenthood' of the # associated DEVS. This validates the coupling determined above. # Internal Coupling: if ((p1.hostDEVS.parent == self and p2.hostDEVS.parent == self) and (p1.type() == 'OUTPORT' and p2.type() == 'INPORT')): if p1.hostDEVS is p2.hostDEVS: raise DEVSException(("In coupled model '%s', connecting ports" + " '%s' and '%s' belong to the same model" + " '%s'. " + " Direct feedback coupling not allowed") % ( self.getModelFullName(), p1.getPortFullName(), p2.getPortFullName(), p1.hostDEVS.getModelFullName())) else: p1.outLine.append(p2) p2.inLine.append(p1) # External input couplings: elif ((p1.hostDEVS == self and p2.hostDEVS.parent == self) and (p1.type() == p2.type() == 'INPORT')): p1.outLine.append(p2) p2.inLine.append(p1) # Eternal output couplings: elif ((p1.hostDEVS.parent == self and p2.hostDEVS == self) and (p1.type() == p2.type() == 'OUTPORT')): p1.outLine.append(p2) p2.inLine.append(p1) # Other cases (illegal coupling): else: raise DEVSException(("Illegal coupling in coupled model '%s' " + "between ports '%s' and '%s'") % ( self.getModelName(), p1.getPortName(), p2.getPortName())) p1.zFunctions[p2] = z if hasattr(self, "server"): self.server.getSelfProxy().dsUndoDirectConnect() class RootDEVS(BaseDEVS): """ The artificial RootDEVS model is the only 'coupled' model in the simulation after direct connection is performed. """ def __init__(self, components, models, schedulerType): """ Basic constructor. :param components: the atomic DEVS models that are the cildren, only those that are ran locally should be mentioned :param models: all models that have to be passed to the scheduler, thus all models, even non-local ones :param schedulerType: type of scheduler to use (string representation) """ BaseDEVS.__init__(self, "ROOT model") self.componentSet = components self.timeNext = (float('inf'), 1) self.local_model_ids = set() for i in self.componentSet: self.local_model_ids.add(i.model_id) self.models = models self.schedulerType = schedulerType self.directConnected = True def directConnect(self): """ Perform direct connection on the models again """ directConnect(self.models, True) def setTimeNext(self): """ Reset the timeNext """ try: self.timeNext = self.scheduler.readFirst() except IndexError: # No element found in the scheduler, so put it to INFINITY self.timeNext = (float('inf'), 1) class Port(object): """ Class for DEVS model ports (both input and output). This class provides basic port attributes and query methods. """ def __init__(self, isInput, name=None): """ Constructor. Creates an input port if isInput evaluates to True, and an output port otherwise. :param isInput: whether or not this is an input port :param name: the name of the port. If None is provided, a unique ID is generated """ self.inLine = [] self.outLine = [] self.hostDEVS = None self.msgcount = 0 # The name of the port self.name = name self.isInput = isInput self.zFunctions = {} def getPortName(self): """ Returns the name of the port :returns: local name of the port """ return self.name def getPortFullName(self): """ Returns the complete name of the port :returns: fully qualified name of the port """ return "%s.%s" % (self.hostDEVS.getModelFullName(), self.getPortName()) def type(self): """ Returns the 'type' of the object :returns: either 'INPORT' or 'OUTPORT' """ if self.isInput: return 'INPORT' else: return 'OUTPORT' def appendZ(first_z, new_z): if first_z is None: return new_z elif new_z is None: return first_z else: return lambda x: new_z(first_z(x)) def directConnect(componentSet, local): """ Perform direct connection on this CoupledDEVS model :param componentSet: the iterable to direct connect :param local: whether or not simulation is local; if it is, dynamic structure code will be prepared :returns: the direct connected componentSet """ newlist = [] for i in componentSet: if isinstance(i, CoupledDEVS): componentSet.extend(i.componentSet) else: # Found an atomic model newlist.append(i) componentSet = newlist # All and only all atomic models are now direct children of this model for i in componentSet: # Remap the output ports for outport in i.OPorts: # The new contents of the line outport.routingOutLine = [] worklist = [(p, outport.zFunctions.get(p, None)) for p in outport.outLine] for outline, z in worklist: # If it is a coupled model, we must expand this model if isinstance(outline.hostDEVS, CoupledDEVS): for inline in outline.outLine: # Add it to the current iterating list, so we can just continue worklist.append((inline, appendZ(z, outline.zFunctions[inline]))) # If it is a Coupled model, we should just continue # expanding it and not add it to the finished line if not isinstance(inline.hostDEVS, CoupledDEVS): outport.routingOutLine.append((inline, appendZ(z, outline.zFunctions[inline]))) else: for ol, z in outport.routingOutLine: if ol == outline: break else: # Add to the new line if it isn't already there # Note that it isn't really mandatory to check for this, # it is a lot cleaner to do so. # This will greatly increase the complexity of the connector though outport.routingOutLine.append((outline, z)) # Remap the input ports: identical to the output ports, only in the reverse direction for inport in i.IPorts: inport.routingInLine = [] for inline in inport.inLine: if isinstance(inline.hostDEVS, CoupledDEVS): for outline in inline.inLine: inport.inLine.append(outline) if not isinstance(outline.hostDEVS, CoupledDEVS): inport.routingInLine.append(outline) elif inline not in inport.routingInLine: inport.routingInLine.append(inline) return componentSet