Ejemplo n.º 1
0
    def __init__(self, name: str, owner = None):
        self.name = name
        self._owner = owner

        # Notifiers
        self.nReceives: Notifier = Notifier('Receives', self)
        self.nConnectsTo: Notifier = Notifier('Connects to', self)
Ejemplo n.º 2
0
def test_notifier_callback(caplog, mocker, simman):
    caplog.set_level(logging.DEBUG, logger='gymwipe.simtools')

    n = Notifier('myNotifier')
    value = "test1"

    # testing callback subscription and invocation
    callHistory = []
    callbackList = []
    for i in range(3, 0, -1):

        def callback(value, i=i):  # force early binding for the i values
            callHistory.append((i, value))
            print(i)

        callbackList.append(callback)

    for priority, c in enumerate(callbackList):
        n.subscribeCallback(c, priority)

    n.trigger(value)

    assert callHistory == [(i, value) for i in range(1, 4)]

    # test unsubscribing
    callHistory = []
    for c in callbackList:
        n.unsubscribeCallback(c)

    assert callHistory == []
Ejemplo n.º 3
0
    def __init__(self,
                 modelClasses: List[AttenuationModelClass],
                 frequency: float = 2.4e9,
                 bandwidth: float = 22e6):
        """
        Args:
            modelClasses: A non-empty list :class:`AttenuationModel` subclasses
                that will be used for attenuation calculations regarding this
                frequency band.
            frequency: The frequency band's frequency in Hz. Defaults to 2.4 GHz.
            bandwidth: The frequency band's bandwidth in Hz. Defaults to 22 MHz (as in
                IEEE 802.11)
        """

        self.spec = FrequencyBandSpec(frequency, bandwidth)
        """
        :class:`FrequencyBandSpec`: The frequency band's specification object
        """

        self._attenuationModelFactory = AttenuationModelFactory(
            self.spec, modelClasses)

        self._transmissions: Deque[Transmission] = deque()
        self._transmissionInReachNotifiers: Dict[Tuple[Device, float],
                                                 Notifier] = {}

        self.nNewTransmission: Notifier = Notifier("New transmission", self)
        """
Ejemplo n.º 4
0
    def __init__(self, frequencyBandSpec: FrequencyBandSpec, deviceA: Device,
                 deviceB: Device):
        """
        Args:
            frequencyBandSpec: The frequency band specification of the corresponding :class:`FrequencyBand`
            deviceA: Network device a
            deviceB: Network device b

        Raises:
            ValueError: If `deviceA` is `deviceB`
        """
        if deviceA is deviceB:
            raise ValueError("An AttenuationModel cannot be created with both "
                             "deviceA and deviceB being the same device.")
        if deviceA.position == deviceB.position:
            if deviceA.name == deviceB.name:
                msg = "{} and {} have the same name and the same position."
            else:
                msg = "{} and {} have the same position."
            logger.warning(
                (msg + " Is this intended?").format(strAndRepr(deviceA),
                                                    strAndRepr(deviceB)))
        self.frequencyBandSpec = frequencyBandSpec
        self.devices: Tuple[Device] = (deviceA, deviceB)
        self.attenuation: float = 0
        """
        float: The attenuation of any signal sent from :class:`NetworkDevice`
        `deviceA` to :class:`NetworkDevice` `deviceB` (or vice versa) at the
        currently simulated time, measured in db.
        """

        self.nAttenuationChanges: Notifier = Notifier("Attenuation changes",
                                                      self)
        """
Ejemplo n.º 5
0
    def __init__(self, name: str, device: Device,
                 frequencyBandSpec: FrequencyBandSpec):
        super(SimpleRrmMac, self).__init__(name, device)
        self._addPort("phy")
        self._addPort("network")
        self.addr = bytes(6)  # 6 zero bytes
        """bytes: The RRM's MAC address"""

        self._announcementMcs = BpskMcs(frequencyBandSpec)
        self._transmissionPower = 0.0  # dBm
        self._nAnnouncementReceived = Notifier(
            "new announcement message received", self)
        self._nAnnouncementReceived.subscribeProcess(self._sendAnnouncement,
                                                     queued=True)

        logger.debug("%s: Initialization completed, MAC address: %s", self,
                     self.addr)
Ejemplo n.º 6
0
    def __init__(self, x: Union[float, int], y: Union[float, int], owner: Any = None):
        """
        Args:
            x: The distance to a fixed origin in x direction, measured in meters
            y: The distance to a fixed origin in y direction, measured in meters
            owner: The object owning (having) the position.
        """
        self._x = float(x)
        self._y = float(y)
        self._owner = owner

        self.nChange: Notifier = Notifier("changes", self)
        """
Ejemplo n.º 7
0
    def __init__(self, name: str, device: Device,
                 frequencyBand: FrequencyBand):
        super(SimplePhy, self).__init__(name, owner=device)
        self.device = device
        self.frequencyBand = frequencyBand
        self._addPort("mac")

        # Attributes related to sending
        self._transmitting = False
        self._currentTransmission = None

        # Attributes related to receiving
        self._receiving = False
        self._nReceivingFinished = Notifier("Finished receiving", self)
        self._currentReceiverMcs = None
        self._resetBitErrorCounter()
        # thermal noise power in mW
        self._thermalNoisePower = self.NOISE_POWER_DENSITY * frequencyBand.spec.bandwidth * 1000
        self._transmissionToReceivedPower: Dict[Transmission, float] = {}
        self._transmissionToAttenuationChangedCallback = {}
        self._receivedPower = self._thermalNoisePower

        def updateReceivedPower(delta: float):
            self._receivedPower += delta
            logger.debug(
                "%s: Received level changed by %s mW, updated to %s mW", self,
                delta, self._receivedPower)

        self._nReceivedPowerChanges = Notifier("Received power changes", self)
        self._nReceivedPowerChanges.subscribeCallback(updateReceivedPower,
                                                      priority=1)

        self.frequencyBand.nNewTransmission.subscribeCallback(
            self._onNewTransmission)
        self.frequencyBand.nNewTransmission.subscribeProcess(self._receive)
        logger.info("Initialized %s with noise power %s dBm", self,
                    milliwattsToDbm(self._thermalNoisePower))
Ejemplo n.º 8
0
    def nNewTransmissionInReach(self, receiver: Device,
                                radius: float) -> Notifier:
        """
        Returns a notifier that is triggered iff a new :class:`Transmission`
        starts whose sender is positioned within the radius specified by
        `radius` around the `receiver`.

        Args:
            receiver: The :class:`NetworkDevice`, around which the radius is
                considered
            radius: The radius around the receiver (in metres)
        """

        if (receiver, radius) in self._transmissionInReachNotifiers:
            return self._transmissionInReachNotifiers[receiver, radius]
        # creating a new notifier otherwise
        n = Notifier(
            "New Transmission within radius {:d} around {}".format(
                radius, receiver), self)
        self._transmissionInReachNotifiers[receiver, radius] = n
        return n
Ejemplo n.º 9
0
def test_notifier_simpy(caplog, simman):
    caplog.set_level(logging.DEBUG, logger='gymwipe.simtools')

    n = Notifier("notifier")
    p1, p2, p3 = [makeLoggingProcess(10) for _ in range(3)]

    n.subscribeProcess(p1, blocking=False)
    n.subscribeProcess(p2, blocking=True, queued=False)
    n.subscribeProcess(p3, blocking=True, queued=True)

    def main():
        for i in range(1, 3):
            n.trigger("msg" + str(i))
            yield SimMan.timeout(1)

    SimMan.process(main())
    SimMan.runSimulation(4)
    # After 4 time units:
    # Two instances of p1 should be running
    assert p1.instanceCounter == 2
    # and the last one should have been started with value "msg2".
    assert p1.value == "msg2"

    # One instance of each p2 and p3 should be running (others are blocked).
    assert p2.instanceCounter == 1
    assert p2.value == "msg1"
    assert p3.instanceCounter == 1
    assert p3.value == "msg1"

    SimMan.runSimulation(11)
    # After 15 time units:
    # All p1 instances should have finished.
    assert p1.instanceCounter == 0

    # The first p2 instance should have finished and no other p2 instance should
    # be running.
    assert p2.instanceCounter == 0
    assert p2.value == "msg1"

    # The second p3 instance should be the only p3 instance at that time.
    assert p3.instanceCounter == 1
    assert p3.value == "msg2"

    # Triggering the notifier again, in order to proof that another instance of
    # p2 will start
    n.trigger("msg3")

    SimMan.runSimulation(1)

    # p2 should be running again, triggered by message 3
    assert p2.instanceCounter == 1
    assert p2.value == "msg3"

    SimMan.runSimulation(25)
    # In the end, all instances should have finished
    assert p1.instanceCounter == 0
    assert p2.instanceCounter == 0
    assert p3.instanceCounter == 0
    # and they all should have processed message 3 at last
    assert p1.value == "msg3"
    assert p2.value == "msg3"
    assert p3.value == "msg3"
Ejemplo n.º 10
0
class SimpleRrmMac(Module):
    """
    The RRM implementation of the protocol described in :class:`SimpleMac`

    The `networkIn` gate accepts objects of the following types:

        * :class:`~gymwipe.networking.messages.Message`

        :class:`~gymwipe.networking.messages.StackMessageTypes`:

            * :attr:`~gymwipe.networking.messages.StackMessageTypes.ASSIGN`

                Send a frequency band assignment announcement that permits a device
                to transmit for a certain time.

                :class:`~gymwipe.networking.messages.Message` args:

                :dest: The 6-byte-long MAC address of the device to be allowed to transmit

                :duration: The number of time steps to assign the frequency band for the specified device
    
    The payloads of packets from other devices are outputted via the `networkOut`
    gate, regardless of their destination address. This enables an interpreter
    to extract observations and rewards for a frequency band assignment learning agent.
    """
    @GateListener.setup
    def __init__(self, name: str, device: Device,
                 frequencyBandSpec: FrequencyBandSpec):
        super(SimpleRrmMac, self).__init__(name, device)
        self._addPort("phy")
        self._addPort("network")
        self.addr = bytes(6)  # 6 zero bytes
        """bytes: The RRM's MAC address"""

        self._announcementMcs = BpskMcs(frequencyBandSpec)
        self._transmissionPower = 0.0  # dBm
        self._nAnnouncementReceived = Notifier(
            "new announcement message received", self)
        self._nAnnouncementReceived.subscribeProcess(self._sendAnnouncement,
                                                     queued=True)

        logger.debug("%s: Initialization completed, MAC address: %s", self,
                     self.addr)

    @GateListener("phyIn", Packet)
    def phyInHandler(self, packet: Packet):
        self.gates["networkOut"].send(packet.payload)

    @GateListener("networkIn", Message)
    def networkInHandler(self, message: Message):
        logger.debug("%s: Got %s.", self, message)
        self._nAnnouncementReceived.trigger(message)

    def _sendAnnouncement(self, assignMessage: Message):
        """
        Is executed by the `_nAnnouncementReceived` notifier in a blocking and
        queued way for every assignMessage that is received on the `networkIn`
        gate.
        """
        destination = assignMessage.args["dest"]
        duration = assignMessage.args["duration"]
        announcement = Packet(SimpleMacHeader(self.addr, destination, flag=1),
                              Transmittable(duration))
        sendCmd = Message(
            StackMessageTypes.SEND, {
                "packet": announcement,
                "power": self._transmissionPower,
                "mcs": self._announcementMcs
            })
        logger.debug("%s: Sending announcement: %s", self, announcement)
        self.gates["phyOut"].send(sendCmd)
        yield sendCmd.eProcessed
        yield SimMan.timeout(
            (duration + 1) *
            TIME_SLOT_LENGTH)  # one extra time slot to prevent collisions

        # mark the current ASSIGN message as processed
        assignMessage.setProcessed()
Ejemplo n.º 11
0
class SimplePhy(Module):
    """
    A physical layer implementation that does not take propagation delays into
    account. It provides a port called `mac` to be connected to a mac layer.
    Slotted time is used, with the length of a time slot being defined by
    :attr:`TIME_SLOT_LENGTH`.
    
    During simulation the frequency band is sensed and every successfully
    received packet is sent via the `macOut` gate.

    The `macIn` gate accepts :class:`~gymwipe.networking.messages.Message` objects
    with the following :class:`~gymwipe.networking.messages.StackMessageTypes`:

    * :attr:`~gymwipe.networking.messages.StackMessageTypes.SEND`

        Send a specified packet on the frequency band.

        :class:`~gymwipe.networking.messages.Message` args:

        :packet: The :class:`~gymwipe.networking.messages.Packet` object
            representing the packet to be sent
        :power: The transmission power in dBm
        :mcs: The :class:`Mcs` object representing the MCS for the transmission
    """

    NOISE_POWER_DENSITY = temperatureToNoisePowerDensity(20.0)
    """float: The receiver's noise power density in Watts/Hertz"""
    @GateListener.setup
    def __init__(self, name: str, device: Device,
                 frequencyBand: FrequencyBand):
        super(SimplePhy, self).__init__(name, owner=device)
        self.device = device
        self.frequencyBand = frequencyBand
        self._addPort("mac")

        # Attributes related to sending
        self._transmitting = False
        self._currentTransmission = None

        # Attributes related to receiving
        self._receiving = False
        self._nReceivingFinished = Notifier("Finished receiving", self)
        self._currentReceiverMcs = None
        self._resetBitErrorCounter()
        # thermal noise power in mW
        self._thermalNoisePower = self.NOISE_POWER_DENSITY * frequencyBand.spec.bandwidth * 1000
        self._transmissionToReceivedPower: Dict[Transmission, float] = {}
        self._transmissionToAttenuationChangedCallback = {}
        self._receivedPower = self._thermalNoisePower

        def updateReceivedPower(delta: float):
            self._receivedPower += delta
            logger.debug(
                "%s: Received level changed by %s mW, updated to %s mW", self,
                delta, self._receivedPower)

        self._nReceivedPowerChanges = Notifier("Received power changes", self)
        self._nReceivedPowerChanges.subscribeCallback(updateReceivedPower,
                                                      priority=1)

        self.frequencyBand.nNewTransmission.subscribeCallback(
            self._onNewTransmission)
        self.frequencyBand.nNewTransmission.subscribeProcess(self._receive)
        logger.info("Initialized %s with noise power %s dBm", self,
                    milliwattsToDbm(self._thermalNoisePower))

    def _getAttenuationModelByTransmission(
            self, t: Transmission) -> AttenuationModel:
        """
        Returns the attenuation model for this device and the sender of the
        transmission `t`.
        """
        return self.frequencyBand.getAttenuationModel(self.device, t.sender)

    def _calculateReceivedPower(self,
                                t: Transmission,
                                attenuation=None) -> float:
        """
        Calculates the power in mW that is received from a certain transmission.

        Args:
            t: The transmission to calculate the received power for
            attenuation: The attenuation between the sender's antenna and the
                antenna of this Phy's device. If not provided, it will be
                requested by the corresponding attenuation model.
        """
        if attenuation is None:
            attenuation = self._getAttenuationModelByTransmission(
                t).attenuation
        return dbmToMilliwatts(t.power - attenuation)

    # Callbacks

    # The purpose of the following three callbacks is to maintain a dict that
    # maps active transmissions to their received power. This is used to
    # calculate signal and noise levels.

    def _onAttenuationChange(self, t: Transmission, attenuation: float):
        """
        Callback that is invoked when the attenuation to the sender of
        `transmission` changes, providing the new attenuation value
        """
        logger.debug("%s: Attenuation to the sender of %s changed to %s dB.",
                     self, t, attenuation)
        newReceivedPower = self._calculateReceivedPower(t, attenuation)
        delta = newReceivedPower - self._transmissionToReceivedPower[t]
        self._transmissionToReceivedPower[t] = newReceivedPower
        self._nReceivedPowerChanges.trigger(delta)

    def _onNewTransmission(self, t: Transmission):
        """
        Is called whenever a transmission starts
        """
        if t is not self._currentTransmission:
            receivedPower = self._calculateReceivedPower(t)
            self._transmissionToReceivedPower[t] = receivedPower
            logger.debug(
                "%s starts, received power from that "
                "transmission: %s mW",
                t,
                receivedPower,
                sender=self)
            self._nReceivedPowerChanges.trigger(receivedPower)
            t.eCompletes.callbacks.append(self._onCompletingTransmission)
            # Subscribe to changes of attenuation for the transmission
            onAttenuationChange = partial(self._onAttenuationChange, t)
            self._transmissionToAttenuationChangedCallback[
                t] = onAttenuationChange
            self._getAttenuationModelByTransmission(
                t).nAttenuationChanges.subscribeCallback(onAttenuationChange)

    def _onCompletingTransmission(self, event: Event):
        """
        Is called when a transmission from another device completes
        """
        t: Transmission = event.value
        assert t in self._transmissionToReceivedPower
        receivedPower = self._transmissionToReceivedPower[t]
        self._transmissionToReceivedPower.pop(t)
        self._nReceivedPowerChanges.trigger(-receivedPower)
        # Unsubscribe from changes of attenuation for the transmission
        callback = self._transmissionToAttenuationChangedCallback.pop(t)
        self._getAttenuationModelByTransmission(
            t).nAttenuationChanges.unsubscribeCallback(callback)

    # Callbacks for bit error calculation

    def _updateBitErrorRate(self, t: Transmission):
        """
        Sets :attr:`_receivedBitErrorRate` to the current bit error rate for the
        transmission `t`.
        """
        signalPower = self._transmissionToReceivedPower[t]
        noisePower = self._receivedPower - signalPower
        assert signalPower >= 0
        assert noisePower >= 0
        signalPowerDbm = milliwattsToDbm(signalPower)
        noisePowerDbm = milliwattsToDbm(noisePower)
        self._receivedBitErrorRate = self._currentReceiverMcs.calculateBitErrorRate(
            signalPowerDbm, noisePowerDbm)
        logger.debug("Currently simulated bit error rate: " +
                     str(self._receivedBitErrorRate),
                     sender=self)

    def _resetBitErrorCounter(self):
        self._receivedBitErrorSum = 0
        self._receivedBitErrorRate = 0.0
        self._lastReceivedErrorCountTime = SimMan.now

    def _countBitErrors(self):
        # Calculate the duration since last time that we counted errors
        now = SimMan.now
        duration = now - self._lastReceivedErrorCountTime

        # Derive the number of bit errors for that duration (as a float,
        # rounding is done in the end)
        bitErrors = self._receivedBitErrorRate * duration * self._currentReceiverMcs.bitRate
        self._receivedBitErrorSum += bitErrors

    # SimPy processes

    @GateListener("macIn", Message, queued=True)
    def macInHandler(self, cmd):
        p = cmd.args

        if cmd.type is StackMessageTypes.SEND:
            logger.info("Received SEND command", sender=self)
            # If the receiver is active, wait until it is inactive again
            if self._receiving:
                yield self._nReceivingFinished.event

            self._transmitting = True
            # Wait for the beginning of the next time slot
            yield SimMan.nextTimeSlot(TIME_SLOT_LENGTH)
            # Simulate transmitting
            t = self.frequencyBand.transmit(self.device, p["power"],
                                            p["packet"], p["mcs"], p["mcs"])
            self._currentTransmission = t
            # Wait for the transmission to finish
            yield t.eCompletes
            self._transmitting = False
            # Indicate that the send command was processed
            cmd.setProcessed()

    def _receive(self, t: Transmission):
        # Simulates receiving via the frequency band
        if not self._transmitting:
            logger.info("Sensed a transmission.", sender=self)
            self._receiving = True
            self._currentReceiverMcs = t.mcsHeader
            self._resetBitErrorCounter()

            # Callback for reacting to changes of the received power
            def onReceivedPowerChange(delta: float):
                if delta != 0:
                    # Count bit errors for the duration in which the power has
                    # not changed
                    self._countBitErrors()

                    if not t.completed:
                        # Update the bit error rate accordingly
                        self._updateBitErrorRate(t)

            self._nReceivedPowerChanges.subscribeCallback(
                onReceivedPowerChange)

            self._updateBitErrorRate(t)  # Calculate initial bitErrorRate

            # Wait for the header to be transmitted
            yield t.eHeaderCompletes

            # Count errors since the last time that the received power has changed
            self._countBitErrors()

            # Decide whether the header could be received
            if self._decide(self._receivedBitErrorSum,
                            t.headerBits,
                            t.mcsHeader,
                            logSubject="Header"):
                # Possibly switch MCS
                self._currentReceiverMcs = t.mcsPayload
                self._resetBitErrorCounter()
                self._updateBitErrorRate(t)

                # Wait for the payload to be transmitted
                yield t.eCompletes
                self._countBitErrors()

                logger.debug("{:.3} of {:.3} payload bits were errors.".format(
                    self._receivedBitErrorSum, t.payloadBits),
                             sender=self)

                # Decide whether the payload could be received
                if self._decide(self._receivedBitErrorSum,
                                t.payloadBits,
                                t.mcsPayload,
                                logSubject="Payload"):
                    # Send the packet via the mac gate
                    self.gates["macOut"].send(t.packet)
                else:
                    logger.info("Receiving transmission payload failed for %s",
                                t,
                                sender=self)

            self._nReceivedPowerChanges.unsubscribeCallback(
                onReceivedPowerChange)
            self._resetBitErrorCounter()
            self._receiving = False
            self._nReceivingFinished.trigger()

    def _decide(self, bitErrorSum, totalBits, mcs, logSubject="Data") -> bool:
        """
        Returns ``True`` if `bitErrorSum` errors can be corrected for
        `totalBits` transmitted bits when applying `mcs`
        """
        bitErrorSum = round(bitErrorSum)
        bitErrorRate = bitErrorSum / totalBits
        maxCorrectableBer = mcs.maxCorrectableBer()
        if bitErrorRate <= maxCorrectableBer:
            logger.info("Decider: {} successfully received "
                        "(bit error rate: {:.3%})".format(
                            logSubject, bitErrorRate),
                        sender=self)
            return True
        else:
            logger.info("Decider: {} received with uncorectable errors "
                        "(bit error rate: {:.3%}, max. correctable "
                        "bit error rate: {:.3%})!".format(
                            logSubject, bitErrorRate, maxCorrectableBer),
                        sender=self)
            return False