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)
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 == []
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) """
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) """
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)
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) """
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 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
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"
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()
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