Module deepcomp.env.entities.station

Expand source code
import structlog
import numpy as np
from shapely.geometry import Polygon
import matplotlib.pyplot as plt

from deepcomp.util.constants import SUPPORTED_SHARING, EPSILON, FAIR_WEIGHT_ALPHA, FAIR_WEIGHT_BETA


# SNR threshold required for UEs to connect to this BS. This threshold corresponds roughly to a distance of 69m.
SNR_THRESHOLD = 2e-8


class Basestation:
    """A base station sending data to connected UEs"""
    def __init__(self, id, pos, sharing_model):
        self.id = id
        self.pos = pos
        self.conn_ues = []
        # model for sharing rate/resources among connected UEs. One of SUPPORTED_SHARING models
        self.sharing_model = sharing_model
        assert self.sharing_model in SUPPORTED_SHARING, f"{self.sharing_model=} not supported. {SUPPORTED_SHARING=}"

        # set constants for SINR and data rate calculation
        # numbers originally from https://sites.google.com/site/lteencyclopedia/lte-radio-link-budgeting-and-rf-planning
        # changed numbers to get shorter range --> simulate smaller map
        self.bw = 9e6   # in Hz?
        self.frequency = 2500    # in MHz
        self.noise = 1e-9   # in mW
        self.tx_power = 30  # in dBm (was 40)
        self.height = 50    # in m
        # just consider downlink for now; more interesting for most apps anyways

        # for visualization: circles around BS that show connection range (69m) and 1 Mbit range (46m); no interference
        self.range_conn = pos.buffer(69)
        self.range_1mbit = pos.buffer(46)
        # also rectangle around pos
        symbol_size = 3
        self.symbol = Polygon([(self.pos.x-symbol_size, self.pos.y-symbol_size),
                               (self.pos.x+symbol_size, self.pos.y-symbol_size),
                               (self.pos.x+symbol_size, self.pos.y+symbol_size),
                               (self.pos.x-symbol_size, self.pos.y+symbol_size)])

        self.log = structlog.get_logger(id=self.id, pos=str(self.pos))
        self.log.info('BS init', sharing_model=self.sharing_model, bw=self.bw, freq=self.frequency, noise=self.noise,
                      tx_power=self.tx_power, height=self.height)

    def __repr__(self):
        return str(self.id)

    @property
    def num_conn_ues(self):
        return len(self.conn_ues)

    @property
    def total_data_rate(self):
        """Total data rate of connections from this BS to all UEs"""
        total_rate = 0
        for ue in self.conn_ues:
            total_rate += ue.bs_dr[self]
        self.log.debug('BS total rate', total_rate=total_rate)
        return total_rate

    @property
    def avg_utility(self):
        """Avg utility of UEs connected to this BS. If the BS is idle, return max utility"""
        if len(self.conn_ues) > 0:
            return np.mean([ue.utility for ue in self.conn_ues])
        return 20

    @property
    def min_utility(self):
        """Min utility of UEs connected to this BS. If the BS is idle, return max utility"""
        if len(self.conn_ues) > 0:
            return min([ue.utility for ue in self.conn_ues])
        return 20

    def plot(self):
        """
        Plot the BS as square with the ID inside as well as circles around it indicating the range.

        :return: A list of created matplotlib artists
        """
        # plot BS
        artists = plt.plot(*self.symbol.exterior.xy, color='black')
        artists.append(plt.annotate(self.id, xy=(self.pos.x, self.pos.y), ha='center', va='center'))
        # plot range
        artists.extend(plt.plot(*self.range_1mbit.exterior.xy, color='black'))
        artists.extend(plt.plot(*self.range_conn.exterior.xy, color='gray'))
        return artists

    def reset(self):
        """Reset BS to having no connected UEs"""
        self.conn_ues = []

    def path_loss(self, distance, ue_height=1.5):
        """Return path loss in dBm to a UE at a given position. Calculation using Okumura Hata, suburban indoor"""
        ch = 0.8 + (1.1 * np.log10(self.frequency) - 0.7) * ue_height - 1.56 * np.log10(self.frequency)
        const1 = 69.55 + 26.16 * np.log10(self.frequency) - 13.82 * np.log10(self.height) - ch
        const2 = 44.9 - 6.55 * np.log10(self.height)
        # add small epsilon to avoid log(0) if distance = 0
        return const1 + const2 * np.log10(distance + EPSILON)

    def received_power(self, distance):
        """Return the received power at a given distance"""
        return 10**((self.tx_power - self.path_loss(distance)) / 10)

    def snr(self, ue_pos):
        """Return the signal-to-noise (SNR) ratio given a UE position."""
        distance = self.pos.distance(ue_pos)
        signal = self.received_power(distance)
        self.log.debug('SNR to UE', ue_pos=str(ue_pos), distance=distance, signal=signal)
        return signal / self.noise

    def data_rate_unshared(self, ue):
        """
        Return the achievable data rate for a given UE assuming that it gets the BS' full, unshared data rate.

        :param ue: UE requesting the achievable data rate
        :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.
        """
        snr = self.snr(ue.pos)
        dr_ue_unshared = self.bw * np.log2(1 + snr)
        return dr_ue_unshared

    def priority(self, ue):
        """
        Priority based on current achievable rate (unshared!) and historic avg rate for proportional-fair sharing.
        https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization

        :param ue: UE for which to calculate the priority
        :return: The calculated priority
        """
        # important: use unshared dr, since the shared dr is again defined based on the priority --> endless recursion
        # add epsilon in denominator to avoid division by 0
        return (self.data_rate_unshared(ue)**FAIR_WEIGHT_ALPHA) / (ue.ewma_dr**FAIR_WEIGHT_BETA + EPSILON)

    def data_rate_shared(self, ue, dr_ue_unshared):
        """
        Return the shared data rate the given UE would get based on its unshared data rate and a sharing model.

        :param ue: UE requesting the achievable data rate
        :param dr_ue_unshared: The UE's unshared achievable data rate
        :return: The UE's final, shared data rate that it (could/does) get from this BS
        """
        assert self.sharing_model in SUPPORTED_SHARING, f"{self.sharing_model=} not supported. {SUPPORTED_SHARING=}"
        dr_ue_shared = None

        # if the UE isn't connected yet, temporarily add it to the connected UEs to properly calculate sharing
        ue_already_conn = ue in self.conn_ues
        if not ue_already_conn:
            # also temporarily add the connection & dr for the UE because it affects its priority (used for prop-fair)
            ue.bs_dr[self] = self.data_rate_unshared(ue)
            self.conn_ues.append(ue)

        # resource-fair = time/bandwidth-fair: split time slots/bandwidth/RBs equally among all connected UEs
        if self.sharing_model == 'resource-fair':
            # split data rate by all already connected UEs incl. this UE
            dr_ue_shared = dr_ue_unshared / self.num_conn_ues

        # rate-fair=volume-fair: rather than splitting the resources equally, all connected UEs get the same rate/volume
        # this makes adding new UEs very expensive if they are far away (leads to much lower shared dr for all UEs)
        if self.sharing_model == 'rate-fair':
            total_inverse_dr = sum([1/self.data_rate_unshared(ue) for ue in self.conn_ues])
            # assume we can split them into infinitely small/many RBs
            dr_ue_shared = 1 / total_inverse_dr

        # capacity maximizing: only send to UE with max dr, not to any other. very unfair, but max BS' dr
        if self.sharing_model == 'max-cap':
            max_ue_idx = np.argmax([self.data_rate_unshared(ue) for ue in self.conn_ues])
            dr_ue_shared = 0
            if self.conn_ues.index(ue) == max_ue_idx:
                dr_ue_shared = self.data_rate_unshared(ue)

        # proportional-fair: https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization
        # calc priority per UE based on its curr achievable dr and its historic avg dr
        # assign RBs proportional to priority
        if self.sharing_model == 'proportional-fair':
            # get UE priority --> fraction of RBs assigned to UE --> corresponding shared dr
            ue_frac_rbs = self.priority(ue) / (sum([self.priority(other_ue) for other_ue in self.conn_ues]) + EPSILON)
            dr_ue_shared = ue_frac_rbs * dr_ue_unshared

        # disconnect UE again if it wasn't connected before
        if not ue_already_conn:
            del ue.bs_dr[self]
            self.conn_ues.remove(ue)

        return dr_ue_shared

    def data_rate(self, ue):
        """
        Return the achievable data rate for a given UE (may or may not be connected already).
        Share & split the achievable data rate among all connected UEs, pretending this UE is also connected.
        :param ue: UE requesting the achievable data rate
        :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.
        """
        # 0 data rate if the UE cannot connect because the SNR is too low
        if not self.can_connect(ue.pos):
            return 0
        # achievable data rate if it wasn't shared with any other connected UEs
        dr_ue_unshared = self.data_rate_unshared(ue)
        # final, shared data rate depends on sharing model
        dr_ue_shared = self.data_rate_shared(ue, dr_ue_unshared)
        self.log.debug('Achievable data rate', ue=ue, dr_ue_unshared=dr_ue_unshared, dr_ue_shared=dr_ue_shared,
                       num_conn_ues=self.num_conn_ues)
        return dr_ue_shared

    def can_connect(self, ue_pos):
        """Return if a UE at a given pos can connect to this BS. That's the case if its SNR is above a threshold."""
        can_connect = self.snr(ue_pos) > SNR_THRESHOLD
        self.log.debug('Can connect?', ue_pos=str(ue_pos), can_connect=can_connect)
        return can_connect

Classes

class Basestation (id, pos, sharing_model)

A base station sending data to connected UEs

Expand source code
class Basestation:
    """A base station sending data to connected UEs"""
    def __init__(self, id, pos, sharing_model):
        self.id = id
        self.pos = pos
        self.conn_ues = []
        # model for sharing rate/resources among connected UEs. One of SUPPORTED_SHARING models
        self.sharing_model = sharing_model
        assert self.sharing_model in SUPPORTED_SHARING, f"{self.sharing_model=} not supported. {SUPPORTED_SHARING=}"

        # set constants for SINR and data rate calculation
        # numbers originally from https://sites.google.com/site/lteencyclopedia/lte-radio-link-budgeting-and-rf-planning
        # changed numbers to get shorter range --> simulate smaller map
        self.bw = 9e6   # in Hz?
        self.frequency = 2500    # in MHz
        self.noise = 1e-9   # in mW
        self.tx_power = 30  # in dBm (was 40)
        self.height = 50    # in m
        # just consider downlink for now; more interesting for most apps anyways

        # for visualization: circles around BS that show connection range (69m) and 1 Mbit range (46m); no interference
        self.range_conn = pos.buffer(69)
        self.range_1mbit = pos.buffer(46)
        # also rectangle around pos
        symbol_size = 3
        self.symbol = Polygon([(self.pos.x-symbol_size, self.pos.y-symbol_size),
                               (self.pos.x+symbol_size, self.pos.y-symbol_size),
                               (self.pos.x+symbol_size, self.pos.y+symbol_size),
                               (self.pos.x-symbol_size, self.pos.y+symbol_size)])

        self.log = structlog.get_logger(id=self.id, pos=str(self.pos))
        self.log.info('BS init', sharing_model=self.sharing_model, bw=self.bw, freq=self.frequency, noise=self.noise,
                      tx_power=self.tx_power, height=self.height)

    def __repr__(self):
        return str(self.id)

    @property
    def num_conn_ues(self):
        return len(self.conn_ues)

    @property
    def total_data_rate(self):
        """Total data rate of connections from this BS to all UEs"""
        total_rate = 0
        for ue in self.conn_ues:
            total_rate += ue.bs_dr[self]
        self.log.debug('BS total rate', total_rate=total_rate)
        return total_rate

    @property
    def avg_utility(self):
        """Avg utility of UEs connected to this BS. If the BS is idle, return max utility"""
        if len(self.conn_ues) > 0:
            return np.mean([ue.utility for ue in self.conn_ues])
        return 20

    @property
    def min_utility(self):
        """Min utility of UEs connected to this BS. If the BS is idle, return max utility"""
        if len(self.conn_ues) > 0:
            return min([ue.utility for ue in self.conn_ues])
        return 20

    def plot(self):
        """
        Plot the BS as square with the ID inside as well as circles around it indicating the range.

        :return: A list of created matplotlib artists
        """
        # plot BS
        artists = plt.plot(*self.symbol.exterior.xy, color='black')
        artists.append(plt.annotate(self.id, xy=(self.pos.x, self.pos.y), ha='center', va='center'))
        # plot range
        artists.extend(plt.plot(*self.range_1mbit.exterior.xy, color='black'))
        artists.extend(plt.plot(*self.range_conn.exterior.xy, color='gray'))
        return artists

    def reset(self):
        """Reset BS to having no connected UEs"""
        self.conn_ues = []

    def path_loss(self, distance, ue_height=1.5):
        """Return path loss in dBm to a UE at a given position. Calculation using Okumura Hata, suburban indoor"""
        ch = 0.8 + (1.1 * np.log10(self.frequency) - 0.7) * ue_height - 1.56 * np.log10(self.frequency)
        const1 = 69.55 + 26.16 * np.log10(self.frequency) - 13.82 * np.log10(self.height) - ch
        const2 = 44.9 - 6.55 * np.log10(self.height)
        # add small epsilon to avoid log(0) if distance = 0
        return const1 + const2 * np.log10(distance + EPSILON)

    def received_power(self, distance):
        """Return the received power at a given distance"""
        return 10**((self.tx_power - self.path_loss(distance)) / 10)

    def snr(self, ue_pos):
        """Return the signal-to-noise (SNR) ratio given a UE position."""
        distance = self.pos.distance(ue_pos)
        signal = self.received_power(distance)
        self.log.debug('SNR to UE', ue_pos=str(ue_pos), distance=distance, signal=signal)
        return signal / self.noise

    def data_rate_unshared(self, ue):
        """
        Return the achievable data rate for a given UE assuming that it gets the BS' full, unshared data rate.

        :param ue: UE requesting the achievable data rate
        :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.
        """
        snr = self.snr(ue.pos)
        dr_ue_unshared = self.bw * np.log2(1 + snr)
        return dr_ue_unshared

    def priority(self, ue):
        """
        Priority based on current achievable rate (unshared!) and historic avg rate for proportional-fair sharing.
        https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization

        :param ue: UE for which to calculate the priority
        :return: The calculated priority
        """
        # important: use unshared dr, since the shared dr is again defined based on the priority --> endless recursion
        # add epsilon in denominator to avoid division by 0
        return (self.data_rate_unshared(ue)**FAIR_WEIGHT_ALPHA) / (ue.ewma_dr**FAIR_WEIGHT_BETA + EPSILON)

    def data_rate_shared(self, ue, dr_ue_unshared):
        """
        Return the shared data rate the given UE would get based on its unshared data rate and a sharing model.

        :param ue: UE requesting the achievable data rate
        :param dr_ue_unshared: The UE's unshared achievable data rate
        :return: The UE's final, shared data rate that it (could/does) get from this BS
        """
        assert self.sharing_model in SUPPORTED_SHARING, f"{self.sharing_model=} not supported. {SUPPORTED_SHARING=}"
        dr_ue_shared = None

        # if the UE isn't connected yet, temporarily add it to the connected UEs to properly calculate sharing
        ue_already_conn = ue in self.conn_ues
        if not ue_already_conn:
            # also temporarily add the connection & dr for the UE because it affects its priority (used for prop-fair)
            ue.bs_dr[self] = self.data_rate_unshared(ue)
            self.conn_ues.append(ue)

        # resource-fair = time/bandwidth-fair: split time slots/bandwidth/RBs equally among all connected UEs
        if self.sharing_model == 'resource-fair':
            # split data rate by all already connected UEs incl. this UE
            dr_ue_shared = dr_ue_unshared / self.num_conn_ues

        # rate-fair=volume-fair: rather than splitting the resources equally, all connected UEs get the same rate/volume
        # this makes adding new UEs very expensive if they are far away (leads to much lower shared dr for all UEs)
        if self.sharing_model == 'rate-fair':
            total_inverse_dr = sum([1/self.data_rate_unshared(ue) for ue in self.conn_ues])
            # assume we can split them into infinitely small/many RBs
            dr_ue_shared = 1 / total_inverse_dr

        # capacity maximizing: only send to UE with max dr, not to any other. very unfair, but max BS' dr
        if self.sharing_model == 'max-cap':
            max_ue_idx = np.argmax([self.data_rate_unshared(ue) for ue in self.conn_ues])
            dr_ue_shared = 0
            if self.conn_ues.index(ue) == max_ue_idx:
                dr_ue_shared = self.data_rate_unshared(ue)

        # proportional-fair: https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization
        # calc priority per UE based on its curr achievable dr and its historic avg dr
        # assign RBs proportional to priority
        if self.sharing_model == 'proportional-fair':
            # get UE priority --> fraction of RBs assigned to UE --> corresponding shared dr
            ue_frac_rbs = self.priority(ue) / (sum([self.priority(other_ue) for other_ue in self.conn_ues]) + EPSILON)
            dr_ue_shared = ue_frac_rbs * dr_ue_unshared

        # disconnect UE again if it wasn't connected before
        if not ue_already_conn:
            del ue.bs_dr[self]
            self.conn_ues.remove(ue)

        return dr_ue_shared

    def data_rate(self, ue):
        """
        Return the achievable data rate for a given UE (may or may not be connected already).
        Share & split the achievable data rate among all connected UEs, pretending this UE is also connected.
        :param ue: UE requesting the achievable data rate
        :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.
        """
        # 0 data rate if the UE cannot connect because the SNR is too low
        if not self.can_connect(ue.pos):
            return 0
        # achievable data rate if it wasn't shared with any other connected UEs
        dr_ue_unshared = self.data_rate_unshared(ue)
        # final, shared data rate depends on sharing model
        dr_ue_shared = self.data_rate_shared(ue, dr_ue_unshared)
        self.log.debug('Achievable data rate', ue=ue, dr_ue_unshared=dr_ue_unshared, dr_ue_shared=dr_ue_shared,
                       num_conn_ues=self.num_conn_ues)
        return dr_ue_shared

    def can_connect(self, ue_pos):
        """Return if a UE at a given pos can connect to this BS. That's the case if its SNR is above a threshold."""
        can_connect = self.snr(ue_pos) > SNR_THRESHOLD
        self.log.debug('Can connect?', ue_pos=str(ue_pos), can_connect=can_connect)
        return can_connect

Instance variables

var avg_utility

Avg utility of UEs connected to this BS. If the BS is idle, return max utility

Expand source code
@property
def avg_utility(self):
    """Avg utility of UEs connected to this BS. If the BS is idle, return max utility"""
    if len(self.conn_ues) > 0:
        return np.mean([ue.utility for ue in self.conn_ues])
    return 20
var min_utility

Min utility of UEs connected to this BS. If the BS is idle, return max utility

Expand source code
@property
def min_utility(self):
    """Min utility of UEs connected to this BS. If the BS is idle, return max utility"""
    if len(self.conn_ues) > 0:
        return min([ue.utility for ue in self.conn_ues])
    return 20
var num_conn_ues
Expand source code
@property
def num_conn_ues(self):
    return len(self.conn_ues)
var total_data_rate

Total data rate of connections from this BS to all UEs

Expand source code
@property
def total_data_rate(self):
    """Total data rate of connections from this BS to all UEs"""
    total_rate = 0
    for ue in self.conn_ues:
        total_rate += ue.bs_dr[self]
    self.log.debug('BS total rate', total_rate=total_rate)
    return total_rate

Methods

def can_connect(self, ue_pos)

Return if a UE at a given pos can connect to this BS. That's the case if its SNR is above a threshold.

Expand source code
def can_connect(self, ue_pos):
    """Return if a UE at a given pos can connect to this BS. That's the case if its SNR is above a threshold."""
    can_connect = self.snr(ue_pos) > SNR_THRESHOLD
    self.log.debug('Can connect?', ue_pos=str(ue_pos), can_connect=can_connect)
    return can_connect
def data_rate(self, ue)

Return the achievable data rate for a given UE (may or may not be connected already). Share & split the achievable data rate among all connected UEs, pretending this UE is also connected. :param ue: UE requesting the achievable data rate :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.

Expand source code
def data_rate(self, ue):
    """
    Return the achievable data rate for a given UE (may or may not be connected already).
    Share & split the achievable data rate among all connected UEs, pretending this UE is also connected.
    :param ue: UE requesting the achievable data rate
    :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.
    """
    # 0 data rate if the UE cannot connect because the SNR is too low
    if not self.can_connect(ue.pos):
        return 0
    # achievable data rate if it wasn't shared with any other connected UEs
    dr_ue_unshared = self.data_rate_unshared(ue)
    # final, shared data rate depends on sharing model
    dr_ue_shared = self.data_rate_shared(ue, dr_ue_unshared)
    self.log.debug('Achievable data rate', ue=ue, dr_ue_unshared=dr_ue_unshared, dr_ue_shared=dr_ue_shared,
                   num_conn_ues=self.num_conn_ues)
    return dr_ue_shared
def data_rate_shared(self, ue, dr_ue_unshared)

Return the shared data rate the given UE would get based on its unshared data rate and a sharing model.

:param ue: UE requesting the achievable data rate :param dr_ue_unshared: The UE's unshared achievable data rate :return: The UE's final, shared data rate that it (could/does) get from this BS

Expand source code
def data_rate_shared(self, ue, dr_ue_unshared):
    """
    Return the shared data rate the given UE would get based on its unshared data rate and a sharing model.

    :param ue: UE requesting the achievable data rate
    :param dr_ue_unshared: The UE's unshared achievable data rate
    :return: The UE's final, shared data rate that it (could/does) get from this BS
    """
    assert self.sharing_model in SUPPORTED_SHARING, f"{self.sharing_model=} not supported. {SUPPORTED_SHARING=}"
    dr_ue_shared = None

    # if the UE isn't connected yet, temporarily add it to the connected UEs to properly calculate sharing
    ue_already_conn = ue in self.conn_ues
    if not ue_already_conn:
        # also temporarily add the connection & dr for the UE because it affects its priority (used for prop-fair)
        ue.bs_dr[self] = self.data_rate_unshared(ue)
        self.conn_ues.append(ue)

    # resource-fair = time/bandwidth-fair: split time slots/bandwidth/RBs equally among all connected UEs
    if self.sharing_model == 'resource-fair':
        # split data rate by all already connected UEs incl. this UE
        dr_ue_shared = dr_ue_unshared / self.num_conn_ues

    # rate-fair=volume-fair: rather than splitting the resources equally, all connected UEs get the same rate/volume
    # this makes adding new UEs very expensive if they are far away (leads to much lower shared dr for all UEs)
    if self.sharing_model == 'rate-fair':
        total_inverse_dr = sum([1/self.data_rate_unshared(ue) for ue in self.conn_ues])
        # assume we can split them into infinitely small/many RBs
        dr_ue_shared = 1 / total_inverse_dr

    # capacity maximizing: only send to UE with max dr, not to any other. very unfair, but max BS' dr
    if self.sharing_model == 'max-cap':
        max_ue_idx = np.argmax([self.data_rate_unshared(ue) for ue in self.conn_ues])
        dr_ue_shared = 0
        if self.conn_ues.index(ue) == max_ue_idx:
            dr_ue_shared = self.data_rate_unshared(ue)

    # proportional-fair: https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization
    # calc priority per UE based on its curr achievable dr and its historic avg dr
    # assign RBs proportional to priority
    if self.sharing_model == 'proportional-fair':
        # get UE priority --> fraction of RBs assigned to UE --> corresponding shared dr
        ue_frac_rbs = self.priority(ue) / (sum([self.priority(other_ue) for other_ue in self.conn_ues]) + EPSILON)
        dr_ue_shared = ue_frac_rbs * dr_ue_unshared

    # disconnect UE again if it wasn't connected before
    if not ue_already_conn:
        del ue.bs_dr[self]
        self.conn_ues.remove(ue)

    return dr_ue_shared
def data_rate_unshared(self, ue)

Return the achievable data rate for a given UE assuming that it gets the BS' full, unshared data rate.

:param ue: UE requesting the achievable data rate :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.

Expand source code
def data_rate_unshared(self, ue):
    """
    Return the achievable data rate for a given UE assuming that it gets the BS' full, unshared data rate.

    :param ue: UE requesting the achievable data rate
    :return: Return the max. achievable data rate for the UE if it were/is connected to the BS.
    """
    snr = self.snr(ue.pos)
    dr_ue_unshared = self.bw * np.log2(1 + snr)
    return dr_ue_unshared
def path_loss(self, distance, ue_height=1.5)

Return path loss in dBm to a UE at a given position. Calculation using Okumura Hata, suburban indoor

Expand source code
def path_loss(self, distance, ue_height=1.5):
    """Return path loss in dBm to a UE at a given position. Calculation using Okumura Hata, suburban indoor"""
    ch = 0.8 + (1.1 * np.log10(self.frequency) - 0.7) * ue_height - 1.56 * np.log10(self.frequency)
    const1 = 69.55 + 26.16 * np.log10(self.frequency) - 13.82 * np.log10(self.height) - ch
    const2 = 44.9 - 6.55 * np.log10(self.height)
    # add small epsilon to avoid log(0) if distance = 0
    return const1 + const2 * np.log10(distance + EPSILON)
def plot(self)

Plot the BS as square with the ID inside as well as circles around it indicating the range.

:return: A list of created matplotlib artists

Expand source code
def plot(self):
    """
    Plot the BS as square with the ID inside as well as circles around it indicating the range.

    :return: A list of created matplotlib artists
    """
    # plot BS
    artists = plt.plot(*self.symbol.exterior.xy, color='black')
    artists.append(plt.annotate(self.id, xy=(self.pos.x, self.pos.y), ha='center', va='center'))
    # plot range
    artists.extend(plt.plot(*self.range_1mbit.exterior.xy, color='black'))
    artists.extend(plt.plot(*self.range_conn.exterior.xy, color='gray'))
    return artists
def priority(self, ue)

Priority based on current achievable rate (unshared!) and historic avg rate for proportional-fair sharing. https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization

:param ue: UE for which to calculate the priority :return: The calculated priority

Expand source code
def priority(self, ue):
    """
    Priority based on current achievable rate (unshared!) and historic avg rate for proportional-fair sharing.
    https://en.wikipedia.org/wiki/Proportionally_fair#User_prioritization

    :param ue: UE for which to calculate the priority
    :return: The calculated priority
    """
    # important: use unshared dr, since the shared dr is again defined based on the priority --> endless recursion
    # add epsilon in denominator to avoid division by 0
    return (self.data_rate_unshared(ue)**FAIR_WEIGHT_ALPHA) / (ue.ewma_dr**FAIR_WEIGHT_BETA + EPSILON)
def received_power(self, distance)

Return the received power at a given distance

Expand source code
def received_power(self, distance):
    """Return the received power at a given distance"""
    return 10**((self.tx_power - self.path_loss(distance)) / 10)
def reset(self)

Reset BS to having no connected UEs

Expand source code
def reset(self):
    """Reset BS to having no connected UEs"""
    self.conn_ues = []
def snr(self, ue_pos)

Return the signal-to-noise (SNR) ratio given a UE position.

Expand source code
def snr(self, ue_pos):
    """Return the signal-to-noise (SNR) ratio given a UE position."""
    distance = self.pos.distance(ue_pos)
    signal = self.received_power(distance)
    self.log.debug('SNR to UE', ue_pos=str(ue_pos), distance=distance, signal=signal)
    return signal / self.noise