# Source code for pypdevs.DEVS

    # Copyright 2014 Modelling, Simulation and Design Lab (MSDL) at
    # McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #    http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.

    """
    Classes and tools for DEVS model specification
    """

    from pypdevs.logger import debug, warn, info, error
    from pypdevs.util import *
    import pypdevs.accurate_time as time

    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.time_last = (0.0, 0)
            self.time_next = (0.0, 1)

            self.location = None

            # Variables used for optimisations
            self.my_input = {}
            self.my_output = {}

            # The state queue, used for time warp
            self.old_states = []
            # List of all memoized states, only useful in distributed simulation
            #   with memoization enabled
            self.memo = []

        def simSettings(self, simulator):
            """
            Modifies the simulation settings from within the model.

            This function is called _before_ direct connection and distribution is performed, so the user can still access the complete hierarchy.

            .. note:: This function is *only* called on the root model of the simulation, thus the model passed to the constructor of the Simulator object.

            :param simulator: the simulator object on which settings can be configured
            """
            pass

        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 getVCDVariables(self):
            """
            Fetch all the variables, suitable for VCD variable generation

            :returns: list -- all variables needed for VCD tracing
            """
            var_list = []
            for I in self.ports:
                var_list.append([self.getModelFullName(), I.getPortName()])
            return var_list

        def removePort(self, port):
            """
            Remove a port (either input or output) from the model, disconnecting all of its connections.

            :param port: the port to remove
            """
            if not hasattr(self, "full_name"):
                raise DEVSException("removePort should only be called during a simulation")
            if port.is_input:
                self.IPorts.remove(port)
            else:
                self.OPorts.remove(port)
            self.ports.remove(port)

            # Also remove all connections to this port
            self.server.getSelfProxy().dsRemovePort(port)

        def addPort(self, name, is_input):
            """
            Utility function to create a new port and add it everywhere where it is necessary

            :param name: the name of the port
            :param is_input: whether or not this is an input port
            """
            name = name if name is not None else "port%s" % len(self.ports)
            port = Port(is_input=is_input, name=name)
            if is_input:
                self.IPorts.append(port)
            else:
                self.OPorts.append(port)
            port.port_id = len(self.ports)
            self.ports.append(port)
            port.host_DEVS = self

            # If simulation is already running, the port should be direct connected too!

            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
            """
            if self.name is None:
                return str(self.model_id)
            else:
                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

            :raises: AttributeError -- when the model is not fully
                                        initialized for simulation
            """
            return self.full_name

        def getModelFullNameRec(self):
            """
            Get the full model name, including the path from the root,
            using recursion.

            :returns: string -- the fully qualified name of the model
            """
            if self.parent is None:
                return self.getModelName()
            return self.parent.getModelFullNameRec() + "." + self.getModelName()

    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
            self.relocatable = True
            self.last_read_time = (0, 0)

        def setLocation(self, location, force=False):
            """
            Sets the location of the atomic DEVS model if it was not already set

            :param location: the location to set
            :param force: whether or not to force this location, even if another is already defined
            """
            if self.location is None or force:
                self.location = location

        def fetchActivity(self, time, activities):
            """
            Fetch the activity of the model up to a certain time

            :param time: the time up to which the activity should be calculated
            :param activities: dictionary containing all activities for the models
            """
            accumulator = 0.0
            for state in self.old_states:
                if state.time_last[0] < time:
                    accumulator += state.activity
            activities[self.model_id] = accumulator

        def setGVT(self, gvt, activities, last_state_only):
            """
            Set the GVT of the model, cleaning up the states vector as required
            for the time warp algorithm

            :param gvt: the new value of the GVT
            :param activities: dictionary containing all activities for the models
            :param last_state_only: whether or not to only use a single state for activity
            """
            copy = None
            activity = 0
            for i in range(len(self.old_states)):
                state = self.old_states[i]
                if state.time_last[0] >= gvt:
                    # Possible that all elements should be kept,
                    # in which case it will return -1 and only keep the last element
                    # So the copy element should be AT LEAST 0
                    copy = max(0, i-1)
                    break
                elif not last_state_only:
                    activity += state.activity
            if self.old_states == []:
                # We have no memory, so we are normally in sequential simulation
                self.old_states = []
            elif copy is None:
                self.old_states = [self.old_states[-1]]
            else:
                self.old_states = self.old_states[copy:]
            if last_state_only:
                activity = self.old_states[0].activity
            activities[self.model_id] = activity

        def revert(self, time, memorize):
            """
            Revert the model to the specified time. All necessary cleanup for this
            model will be done (fossil collection).

            :param time: the time up to which should be reverted
            :param memorize: whether or not the saved states should still be kept for memoization
            """
            new_state = len(self.old_states) - 1
            for state in reversed(self.old_states[1:]):
                if state.time_last < time:
                    break
                new_state -= 1

            state = self.old_states[new_state]
            self.time_last = state.time_last
            self.time_next = state.time_next

            self.state = state.loadState()
            if memorize:
                # Reverse it too
                self.memo = self.old_states[:-len(self.old_states) + new_state - 1:-1]
            self.old_states = self.old_states[:new_state + 1]

            # Check if one of the reverted states was ever read for the termination condition
            if self.last_read_time > time:
                # It seems it was, so notify the main revertion algorithm of this
                self.last_read_time = (0, 0)
                return True
            else:
                return False

            # NOTE clearing the myInput happens in the parent

        def getState(self, request_time, first_call=True):
            """
            For the distributed termination condition: fetch the state of the model at a certain time

            :param request_time: the time (including age!) for which the state should be fetched
            :param first_call: whether or not this is the first call of a possible recursive call
            :returns: state -- the state at that time
            """
            if self.location != MPIRedirect.local.name:
                return getProxy(self.location).getStateAtTime(self.model_id,
                                                              request_time)
            elif first_call:
                # Shortcut if the call is local
                return self.state
            self.last_read_time = request_time
            while 1:
                for state in self.old_states:
                    if state.time_last > request_time:
                        return state.loadState()
                # State not yet available... wait some time before trying again...
                time.sleep(0.01)

        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.
            The elapsed time is accessible as an attribute (i.e., self.elapsed), see :ref:`elapsed_time` for a detailed explanation.

            .. 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 preActivityCalculation(self):
            """
            DEFAULT pre-transition activity fetcher. The returned value is passed to the *postActivityCalculation* function

            :returns: something -- passed to the *postActivityCalculation*
            """
            return time.time()

        def postActivityCalculation(self, prevalue):
            """
            DEFAULT post-transition activity fetcher. The returned value will be passed on to the relocator and MUST be an addable (e.g. integer, float, ...)

            :param prevalue: the value returned from the *preActivityCalculation* method
            :returns: addable (float, integer, ...) -- passed to the relocator
            """
            return time.time() - prevalue

        def flattenConnections(self):
            """
            Flattens the pickling graph, by removing backreference from the ports.
            """
            # It doesn't really matter what gets written in these hostDEVS attributes,
            # as it will never be used. Though for readability, the model_id will be used
            # to make it possible to do some debugging when necessary.
            for port in self.IPorts:
                port.host_DEVS = self.model_id
            for port in self.OPorts:
                port.host_DEVS = self.model_id

        def unflattenConnections(self):
            """
            Unflattens the picking graph, by reconstructing backreferences from the ports.
            """
            for port in self.IPorts:
                port.host_DEVS = self
            for port in self.OPorts:
                port.host_DEVS = self

        def finalize(self, name, model_counter, model_ids, locations, select_hierarchy):
            """
            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 select_hierarchy: hierarchy to perform selections in Classic DEVS

            :returns: int -- the new model ID counter
            """
            # Give a name
            self.full_name = name + str(self.getModelName())

            # Give a unique ID to the model itself
            self.model_id = model_counter
            self.select_hierarchy = select_hierarchy + [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

        def getModelLoad(self, lst):
            """
            Add this atomic model to the load of its location

            :param lst: list containing all locations and their current load
            :returns: int -- number of children in this subtree
            """
            lst[self.location] += 1
            self.num_children = 1
            return self.num_children

    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.component_set = []

        def forceSequential(self):
            """
            Force a sequential simulation
            """
            self.setLocation(0, force=True)

        def select(self, imm_children):
            """
            DEFAULT select function, only used when using Classic DEVS simulation

            :param imm_children: list of all children that want to transition
            :returns: child -- a single child that is allowed to transition
            """
            return imm_children[0]

        def getModelLoad(self, lst):
            """
            Fetch the number of atomic models at this model

            :param lst: list containing all locations and their current load
            :returns: number of atomic models in this subtree, including non-local ones
            """
            children = 0
            for i in self.component_set:
                children += i.getModelLoad(lst)
            self.num_children = children
            return self.num_children

        def finalize(self, name, model_counter, model_ids, locations, select_hierarchy):
            """
            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 select_hierarchy: hierarchy to perform selections in Classic DEVS

            :returns: int -- the new model ID counter
            """
            # Set name, even though it will never be requested
            self.full_name = name + str(self.getModelName())
            for i in self.component_set:
                model_counter = i.finalize(self.full_name + ".", model_counter,
                        model_ids, locations, select_hierarchy + [self])
            return model_counter

        def flattenConnections(self):
            """
            Flattens the pickling graph, by removing backreference from the ports.
            """
            for i in self.component_set:
                i.flattenConnections()

        def unflattenConnections(self):
            """
            Unflattens the pickling graph, by reconstructing backreference from the ports.
            """
            for i in self.component_set:
                i.unflattenConnections()

        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.component_set:
                    i.setLocation(model.location)
            if hasattr(self, "server"):
                self.server.getSelfProxy().dsScheduleModel(model)
            else:
                self.component_set.append(model)
            return model

        def removeSubModel(self, model):
            """
            Remove a specified model from the current coupled model, only callable while in a simulation.

            :param model: the model to remove as a child
            """
            if not hasattr(self, "full_name"):
                raise DEVSException("removeSubModel can only be called _during_ a simulation run")
            self.server.getSelfProxy().dsUnscheduleModel(model)

        def disconnectPorts(self, p1, p2):
            """
            Disconnect two ports

            .. note:: If these ports are connected multiple times, **only one** of them will be removed.

            :param p1: the port at the start of the connection
            :param p2: the port at the end of the connection
            """
            if not hasattr(self, "full_name"):
                raise DEVSException("removeSubModel can only be called _during_ a simulation run")
            new_connection = []
            found = False
            for p in p1.outline:
                if p == p2 and not found:
                    found = True
                else:
                    new_connection.append(p)
            p1.outline = new_connection
            new_connection = []
            found = False
            for p in p2.inline:
                if p == p1 and not found:
                    found = True
                else:
                    new_connection.append(p)
            p2.inline = new_connection
            self.server.getSelfProxy().dsDisconnectPorts(p1, p2)

        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.host_DEVS.parent == self and p2.host_DEVS.parent == self) and
                    (p1.type() == 'OUTPORT' and p2.type() == 'INPORT')):
                if p1.host_DEVS is p2.host_DEVS:
                    raise DEVSException(("In coupled model '%s', connecting ports" +
                                        " '%s' and '%s' belong to the same model" +
                                        " '%s'. " +
                                        " Direct feedback coupling not allowed") % (
                                        self.getModelName(),
                                        p1.getPortName(),
                                        p2.getPortName(),
                                        p1.host_DEVS.getModelName()))
                else:
                    p1.outline.append(p2)
                    p2.inline.append(p1)

            # External input couplings:
            elif ((p1.host_DEVS == self and p2.host_DEVS.parent == self) and
                  (p1.type() == p2.type() == 'INPORT')):
                p1.outline.append(p2)
                p2.inline.append(p1)

            # Eternal output couplings:
            elif ((p1.host_DEVS.parent == self and p2.host_DEVS == 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.z_functions[p2] = z
            if hasattr(self, "full_name"):
                # TODO modify
                self.server.getSelfProxy().dsConnectPorts(p1, p2)

        def setLocation(self, location, force=False):
            """
            Sets the location of this coupled model and its submodels if they don't have their own preference.

            :param location: the location to set
            :param force: whether or not to force this location, even if another is already defined
            """
            if self.location is None or force:
                self.location = location
                for child in self.component_set:
                    child.setLocation(location, force)

    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, scheduler_type):
            """
            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 scheduler_type: type of scheduler to use (string representation)
            """
            BaseDEVS.__init__(self, "ROOT model")
            self.component_set = components
            self.time_next = (float('inf'), 1)
            self.local_model_ids = set([None])
            for i in self.component_set:
                self.local_model_ids.add(i.model_id)
            self.models = models
            self.scheduler_type = scheduler_type
            self.listeners = {}

        def redoDirectConnection(self, ports):
            """
            Redo direct connection for a specified port, and all ports connected to it.

            :param ports: the ports that have changed.
            """
            # Find all changed ports and redo their direct connection
            worklist = list(ports)
            for outport in worklist:
                worklist.extend(outport.inline)

            for p in set(worklist):
                directConnectPort(p, self.listeners)

        def directConnect(self):
            """
            Perform direct connection on the models again
            """
            directConnect(self.models, self.listeners)

        def setScheduler(self, scheduler_type):
            """
            Set the scheduler to the desired type. Will overwite the previously present scheduler.

            :param scheduler_type: type of scheduler to use (string representation)
            """
            if isinstance(scheduler_type, tuple):
                try:
                    exec("from pypdevs.schedulers.%s import %s" % scheduler_type)
                except:
                    exec("from %s import %s" % scheduler_type)
                nr_models = len(self.models)
                self.scheduler = eval("%s(self.component_set, EPSILON, nr_models)"
                                      % scheduler_type[1])
            else:
                raise DEVSException("Unknown Scheduler: " + str(scheduler_type))

        def setGVT(self, gvt, activities, last_state_only):
            """
            Sets the GVT of this coupled model

            :param gvt: the time to which the GVT should be set
            :param activities: dictionary containing all activities for the models
            :param last_state_only: whether or not to use the last state for activity
            """
            for i in self.component_set:
                i.setGVT(gvt, activities, last_state_only)

        def fetchActivity(self, time, activities):
            """
            Fetch the activity of the model up to a certain time

            :param time: the time up to which the activity should be calculated
            :param activities: dictionary containing all activities for the models
            """
            for i in self.component_set:
                i.fetchActivity(time, activities)

        def revert(self, time, memorize):
            """
            Revert the coupled model to the specified time, all submodels will also
            be reverted.

            :param time: the time up to which revertion should happen
            :param memorize: whether or not the saved states should still be kept for memoization
            """
            reschedules = set()
            controller_revert = False
            for child in self.component_set:
                if child.time_last >= time:
                    controller_revert |= child.revert(time, memorize)
                    # Was reverted, so reschedule
                    reschedules.add(child)
                # Always clear the inputs, as it is possible that there are only
                # partial results, which doesn't get found in the time_last >= time
                child.my_input = {}
            self.scheduler.massReschedule(reschedules)
            self.setTimeNext()
            return controller_revert

        def setTimeNext(self):
            """
            Reset the timeNext
            """
            try:
                self.time_next = self.scheduler.readFirst()
            except IndexError:
                # No element found in the scheduler, so put it to INFINITY
                self.time_next = (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, is_input, name=None):
            """
            Constructor. Creates an input port if isInput evaluates to True, and
            an output port otherwise.

            :param is_input: 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.host_DEVS = None
            self.msg_count = 0

            # The name of the port
            self.name = name
            self.is_input = is_input
            self.z_functions = {}

        def __repr__(self):
            return "%s (%s)" % (self.type(), self.getPortFullName())

        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.host_DEVS.getModelFullNameRec(), self.getPortName())

        def type(self):
            """
            Returns the 'type' of the object

            :returns: either 'INPORT' or 'OUTPORT'
            """
            if self.is_input:
                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))

    class ExternalWrapper(AtomicDEVS):
        def __init__(self, function):
            AtomicDEVS.__init__(self, "Fake")
            self.f = function
            self.model_id = None
            self.full_name = None

        def extTransition(self, inputs):
            # Fake object is created with a single fake port, so unpack that
            self.f(self.my_input.values()[0])

    def directConnect(component_set, listeners):
        """
        Perform direct connection on this CoupledDEVS model

        :param component_set: the iterable to direct connect
        :returns: the direct connected component_set
        """
        new_list = []
        for i in component_set:
            if isinstance(i, CoupledDEVS):
                component_set.extend(i.component_set)
            else:
                # Found an atomic model
                new_list.append(i)
        component_set = new_list

        # All and only all atomic models are now direct children of this model
        listener_keys = set(listeners.keys())
        for i in component_set:
            # Remap the output ports
            for outport in i.OPorts:
                # The new contents of the line
                outport.routing_outline = []
                worklist = [(p, outport.z_functions.get(p, None))
                            for p in outport.outline]
                for outline, z in worklist:
                    if outline in listener_keys:
                        # This port is being listened on, so just add it as a fake model
                        fake_port = Port(is_input=False,name="Fake")
                        fake_port.host_DEVS = ExternalWrapper(listeners[outline])
                        outport.routing_outline.append((fake_port, z))
                    # If it is a coupled model, we must expand this model
                    if isinstance(outline.host_DEVS, CoupledDEVS):
                        for inline in outline.outline:
                            # Add it to the current iterating list, so we can just continue
                            entry = (inline, appendZ(z, outline.z_functions[inline]))
                            worklist.append(entry)
                            # If it is a Coupled model, we should just continue
                            # expanding it and not add it to the finished line
                            if not isinstance(inline.host_DEVS, CoupledDEVS):
                                entry = (inline, appendZ(z, outline.z_functions[inline]))
                                outport.routing_outline.append(entry)
                    else:
                        for ol, z in outport.routing_outline:
                            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.routing_outline.append((outline, z))
        return component_set

    def directConnectPort(outport, listeners):
        """
        Perform direct connection on a single port.

        :param outpurt: the port to reconnect
        :param listeners: the listeners that exist, potentially on this port
        :returns: None
        """

        # The new contents of the line
        outport.routing_outline = []
        worklist = [(p, outport.z_functions.get(p, None))
                    for p in outport.outline]
        listener_keys = set(listeners.keys())
        for outline, z in worklist:
            if outline in listener_keys:
                # This port is being listened on, so just add it as a fake model
                fake_port = Port(is_input=False,name="Fake")
                fake_port.host_DEVS = ExternalWrapper(listeners[outline])
                outport.routing_outline.append((fake_port, z))

            # If it is a coupled model, we must expand this model
            if isinstance(outline.host_DEVS, CoupledDEVS):
                for inline in outline.outline:
                    # Add it to the current iterating list, so we can just continue
                    entry = (inline, appendZ(z, outline.z_functions.get(inline, None)))
                    worklist.append(entry)
                    # If it is a Coupled model, we should just continue
                    # expanding it and not add it to the finished line
                    if not isinstance(inline.host_DEVS, CoupledDEVS):
                        entry = (inline, appendZ(z, outline.z_functions.get(inline, None)))
                        outport.routing_outline.append(entry)
            else:
                for ol, z in outport.routing_outline:
                    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.routing_outline.append((outline, z))