def test_invalidating_earlier_choice_impossible(): """ Tests that an action that would invalidate an earlier action is impossible. """ infra = InfrastructureNetwork() # Two sources, one sink. Equal distance from both sources to sink. # One source with moderate transmit power (but enough to cover the # distance, one source with excessive transmit power. # transmit_power_dbm # power of 30dBm (similar to a regular router) which should easily # cover the distance of 1m without any noise. source_node_silent = infra.add_source( pos=(0, 0), transmit_power_dbm=20, name="Silent" ) source_node_screamer = infra.add_source( pos=(3, 0), transmit_power_dbm=100, name="Screamer" ) node_sink = infra.set_sink(pos=(1, 3), transmit_power_dbm=0, name="Sink") overlay = OverlayNetwork() esource_silent = ENode(overlay.add_source(), source_node_silent) esource_screamer = ENode(overlay.add_source(), source_node_screamer) esink = ENode(overlay.set_sink(), node_sink) overlay.add_link(esource_silent.block, esink.block) overlay.add_link(esource_screamer.block, esink.block) embedding = PartialEmbedding( infra, overlay, source_mapping=[ (esource_silent.block, esource_silent.node), (esource_screamer.block, esource_screamer.node), ], ) action_to_be_invalidated = (esource_screamer, esink, 0) # make sure the action is an option in the first place assert action_to_be_invalidated in embedding.possibilities() # embed the link from the silent node to the sink embedding.take_action(esource_silent, esink, 0) # first assert that action would be valid by itself screamer_sinr = embedding.known_sinr(source_node_screamer, node_sink, 0) assert screamer_sinr > 2.0 new_possibilities = embedding.possibilities() # but since the action would make the first embedding invalid (a # node cannot receive two signals at the same time), it should still # not be possible assert action_to_be_invalidated not in new_possibilities # since there are no options left in the first timeslot, there are # now exactly 2 (screamer -> silent as relay, screamer -> sink # embedded) options left in the newly created second timeslot assert len(new_possibilities) == 2
def test_block_capacity(): """Tests that per-node capacity is respected for each timeslot""" infra = InfrastructureNetwork() nso = infra.add_source(pos=(0, 0), transmit_power_dbm=30, name="nso") nin1 = infra.add_intermediate( pos=(-1, 1), transmit_power_dbm=30, capacity=42, name="nin1" ) nin2 = infra.add_intermediate( pos=(1, 1), transmit_power_dbm=30, capacity=5, name="nin2" ) _nsi = infra.set_sink(pos=(0, 1), transmit_power_dbm=30, name="nsi") overlay = OverlayNetwork() # ignore sinr constraints -> 0 datarate requirements bso = overlay.add_source(name="bso", datarate=0) bin1 = overlay.add_intermediate(requirement=40, name="bin1", datarate=0) bin2 = overlay.add_intermediate(requirement=5, name="bin2", datarate=0) bsi = overlay.set_sink(name="bsi", datarate=0) overlay.add_link(bso, bin1) overlay.add_link(bso, bin2) overlay.add_link(bin1, bsi) overlay.add_link(bin2, bsi) embedding = PartialEmbedding(infra, overlay, source_mapping=[(bso, nso)]) eso = ENode(bso, nso) possibilities = embedding.possibilities() # bin1 can be embedded in nin1, because 42>=40 assert (eso, ENode(bin1, nin1), 0) in possibilities # but not in nin2 because it does not have enough capacity assert (eso, ENode(bin1, nin2), 0) not in possibilities # bin2 has less requirements and can be embedded in either one assert (eso, ENode(bin2, nin1), 0) in possibilities assert (eso, ENode(bin2, nin2), 0) in possibilities # embed bin1 in nin1 assert embedding.take_action(ENode(bso, nso), ENode(bin1, nin1), 0) possibilities = embedding.possibilities() # pylint:disable=protected-access # The easiest way to test this, not too hard to adjust when # internals change. assert embedding._capacity_used[nin1] == 40 # which means bin2 can no longer be embedded in it assert (eso, ENode(bin2, nin1), 0) not in possibilities # while it can still be embedded in nin2 assert (eso, ENode(bin2, nin2), 0) in possibilities
def test_unnecessary_links_removed_in_other_timeslots(): """ Tests that links in other timeslots are removed if they are embedded in one timeslot. """ infra = InfrastructureNetwork() nfaraway_1 = infra.add_source( pos=(999999998, 99999999), transmit_power_dbm=5, name="nfaraway_1" ) nfaraway_2 = infra.add_intermediate( pos=(999999999, 99999999), transmit_power_dbm=5, name="nfaraway_2" ) nsi = infra.set_sink(pos=(9, 5), transmit_power_dbm=12, name="nsi") nso = infra.add_source(pos=(8, 3), transmit_power_dbm=3, name="nso") overlay = OverlayNetwork() bsi = overlay.set_sink(name="bsi") bso = overlay.add_source(name="bso") bfaraway_1 = overlay.add_source(name="bfaraway_1") bfaraway_2 = overlay.add_intermediate(name="bfaraway_2", datarate=0) overlay.add_link(bso, bsi) overlay.add_link(bfaraway_1, bfaraway_2) # just to make it correct overlay.add_link(bfaraway_2, bsi) embedding = PartialEmbedding( infra, overlay, source_mapping=[(bso, nso), (bfaraway_1, nfaraway_1)] ) esi = ENode(bsi, nsi) eso = ENode(bso, nso) efaraway_1 = ENode(bfaraway_1, nfaraway_1) efaraway_2 = ENode(bfaraway_2, nfaraway_2) # make sure a second timeslot is created assert embedding.take_action(efaraway_1, efaraway_2, 0) # make sure embedding is possible in ts1 assert (eso, esi, 1) in embedding.possibilities() # embed the link in ts 0 assert embedding.take_action(eso, esi, 0) # now no embedding in another timeslot should be possible anymore possible_outlinks_from_eso = [ pos for pos in embedding.possibilities() if pos[0] == eso ] assert len(possible_outlinks_from_eso) == 0
def test_timeslots_dynamically_created(): """Tests the dynamic creation of new timeslots as needed""" infra = InfrastructureNetwork() nso1 = infra.add_source( name="nso1", pos=(0, 0), # transmits so loudly that no other node can realistically # transmit in the same timeslot transmit_power_dbm=1000, ) nso2 = infra.add_source(name="nso2", pos=(1, 0), transmit_power_dbm=1000) nsi = infra.set_sink(name="nsi", pos=(1, 1), transmit_power_dbm=1000) overlay = OverlayNetwork() bso1 = overlay.add_source(name="bso1") bso2 = overlay.add_source(name="bso2") bsi = overlay.set_sink(name="bsi") eso1 = ENode(bso1, nso1) esi = ENode(bsi, nsi) overlay.add_link(bso1, bsi) overlay.add_link(bso2, bsi) embedding = PartialEmbedding( infra, overlay, source_mapping=[(bso1, nso1), (bso2, nso2)] ) # nothing used yet assert embedding.used_timeslots == 0 # it would be possible to create a new timeslot and embed either # link in it (2) or go to a relay from either source (2) assert len(embedding.possibilities()) == 4 # Take an action. nosurce1 will transmit so strongly that nso2 # cannot send at the same timelot assert embedding.take_action(eso1, esi, 0) # timeslot 0 is now used assert embedding.used_timeslots == 1 # New options (for creating timeslot 1) were created accordingly. # The second source could now still send to the other source as a # relay or to to the sink directly, it will just have to do it in a # new timeslot. assert len(embedding.possibilities()) == 2
def test_non_broadcast_parallel_communications_impossible(): """Tests that non-broadcast parallel communications *do* affect the SINR.""" infra = InfrastructureNetwork() nso1 = infra.add_source(pos=(1, 0), transmit_power_dbm=30, name="nso1") nso2 = infra.add_source(pos=(-1, 0), transmit_power_dbm=30, name="nso2") nin = infra.add_intermediate(pos=(1, 0), transmit_power_dbm=30, name="nin") nsi = infra.set_sink(pos=(2, 0), transmit_power_dbm=30, name="nsi") overlay = OverlayNetwork() bso1 = overlay.add_source(name="bso1") bso2 = overlay.add_source(name="bso2") bsi = overlay.set_sink(name="bsi") overlay.add_link(bso1, bsi) overlay.add_link(bso2, bsi) embedding = PartialEmbedding( infra, overlay, source_mapping=[(bso1, nso1), (bso2, nso2)] ) # both sources use nin as a relay eso1 = ENode(bso1, nso1) eso2 = ENode(bso2, nso2) esi = ENode(bsi, nsi) ein1 = ENode(bso1, nin, bsi) ein2 = ENode(bso2, nin, bsi) assert embedding.take_action(eso1, ein1, 0) assert embedding.take_action(eso2, ein2, 1) assert embedding.take_action(ein1, esi, 2) assert (ein2, esi, 2) not in embedding.possibilities()
def test_trivial_possibilities(): """ Tests that a single reasonable option is correctly generated in a trivial case. """ infra = InfrastructureNetwork() # Two nodes, 1m apart. The transmitting node has a # transmit_power_dbm # power of 30dBm (similar to a regular router) which should easily # cover the distance of 1m without any noise. source_node = infra.add_source(pos=(0, 0), transmit_power_dbm=0.1) infra.set_sink(pos=(1, 0), transmit_power_dbm=0) overlay = OverlayNetwork() source_block = overlay.add_source() sink_block = overlay.set_sink() overlay.add_link(source_block, sink_block) embedding = PartialEmbedding( infra, overlay, source_mapping=[(source_block, source_node)] ) # can only embed B2 into N2 assert len(embedding.possibilities()) == 1
def test_path_loss(): """ Tests that an embedding over impossible distances is recognized as invalid. """ infra = InfrastructureNetwork() # Two nodes, 1km apart. The transmitting node has a transmission # power of 1dBm (=1.26mW). With a path loss over 1km of *at least* # 30dBm, less than ~-30dBm (approx. 10^-3 = 0.001mW = 1uW) arrives # at the target. That is a very optimistic approximation and is not # nearly enough to send any reasonable signal. source_node = infra.add_source(pos=(0, 0), transmit_power_dbm=1) infra.set_sink(pos=(1000, 0), transmit_power_dbm=0) overlay = OverlayNetwork() source_block = overlay.add_source() sink_block = overlay.set_sink() overlay.add_link(source_block, sink_block) embedding = PartialEmbedding( infra, overlay, source_mapping=[(source_block, source_node)] ) assert len(embedding.possibilities()) == 0
def get_random_action(embedding: PartialEmbedding, rand): """Take a random action on the given partial embedding""" possibilities = embedding.possibilities() if len(possibilities) == 0: return None choice = rand.randint(0, len(possibilities)) return possibilities[choice]
def test_outlinks_limited(): """ Tests that the number of possible outlinks is limited by the number of outlinks to embed for that block. """ infra = InfrastructureNetwork() nsource = infra.add_source(pos=(0, 0), transmit_power_dbm=1, name="nso") nrelay = infra.add_intermediate( pos=(1, 0), transmit_power_dbm=1, name="nr" ) # The sink is way out of reach, embedding is not possible _nsink = infra.set_sink(pos=(1, 1), transmit_power_dbm=1, name="nsi") overlay = OverlayNetwork() bsource = overlay.add_source(name="bso") bsink = overlay.set_sink(name="bsi") esource = ENode(bsource, nsource) overlay.add_link(bsource, bsink) embedding = PartialEmbedding( infra, overlay, source_mapping=[(esource.block, esource.node)] ) erelay = ENode(bsource, nrelay, bsink) assert embedding.take_action(esource, erelay, 0) possibilities_from_source = [ (source, target, timeslot) for (source, target, timeslot) in embedding.possibilities() if source == esource ] # the source block has one outgoing edge, one outlink is already # embedded (although the link is not embedded completely) assert len(possibilities_from_source) == 0 possibilities_from_relay = [ (source, target, timeslot) for (source, target, timeslot) in embedding.possibilities() if source == erelay ] # yet the link can be continued from the relay assert len(possibilities_from_relay) > 0
def test_all_viable_options_offered(): """ Tests that all manually verified options are offered in a concrete example. """ infra = InfrastructureNetwork() # Two sources, one sink, one intermediate, one relay # Enough transmit power so that it doesn't need to be taken into account nso1 = infra.add_source( pos=(0, 0), # transmit power should not block anything in this example transmit_power_dbm=100, name="nso1", ) nso2 = infra.add_source(pos=(1, 0), transmit_power_dbm=100, name="nso2") _nrelay = infra.add_intermediate( pos=(0, 1), transmit_power_dbm=100, name="nr" ) _ninterm = infra.add_intermediate( pos=(2, 0), transmit_power_dbm=100, name="ni" ) _nsink = infra.set_sink(pos=(1, 1), transmit_power_dbm=100, name="nsi") overlay = OverlayNetwork() bso1 = overlay.add_source(name="bso1") bso2 = overlay.add_source(name="bso2") bsi = overlay.set_sink(name="bsi") bin_ = overlay.add_intermediate(name="bin") eso1 = ENode(bso1, nso1) eso2 = ENode(bso2, nso2) # source1 connects to the sink over the intermediate source2 # connects both to the sink and to source1. overlay.add_link(bso1, bin_) overlay.add_link(bin_, bsi) overlay.add_link(bso2, bsi) overlay.add_link(bso2, bso1) embedding = PartialEmbedding( infra, overlay, source_mapping=[(bso1, eso1.node), (bso2, eso2.node)] ) # source1 can connect to the intermediate, which could be embedded # in any node (5). It could also connect to any other node as a # relay (4) -> 9. source2 can connect to the sink (1) or the other # source (1). It could also connect to any other node as a relay for # either of its two links (2 * 3) -> 8 No timeslot is used yet, so # there is just one timeslot option. assert len(embedding.possibilities()) == 9 + 8
def test_no_unnecessary_options(): """ Tests that no unnecessary connections are offered. """ infra = InfrastructureNetwork() # Two sources, one sink. Equal distance from both sources to sink. # One source with moderate transmit power (but enough to cover the # distance, one source with excessive transmit power. # transmit_power_dbm # power of 30dBm (similar to a regular router) which should easily # cover the distance of 1m without any noise. source_node = infra.add_source( pos=(0, 0), transmit_power_dbm=30, name="Source" ) sink_node = infra.set_sink(pos=(1, 3), transmit_power_dbm=0, name="Sink") overlay = OverlayNetwork() esource = ENode(overlay.add_source(), source_node) esink = ENode(overlay.set_sink(), sink_node) overlay.add_link(esource.block, esink.block) embedding = PartialEmbedding( infra, overlay, source_mapping=[(esource.block, esource.node)] ) assert len(embedding.possibilities()) == 1 # embed the sink embedding.take_action(esource, esink, 0) # Now it would still be *feasible* according to add a connection to # the relay in the other timeslot. It shouldn't be possible however, # since all outgoing connections are already embedded. assert len(embedding.possibilities()) == 0
def test_half_duplex(): """Tests that a node cannot send and receive at the same time""" infra = InfrastructureNetwork() nso = infra.add_source(name="nso", pos=(0, 0), transmit_power_dbm=30) ni = infra.add_intermediate(name="ni", pos=(1, 0), transmit_power_dbm=30) nsi = infra.set_sink(name="nsi", pos=(2, 0), transmit_power_dbm=30) overlay = OverlayNetwork() # links have no datarate requirements, so SINR concerns don't apply bso = overlay.add_source(name="bso", datarate=0) bsi = overlay.set_sink(name="bsi", datarate=0) overlay.add_link(bso, bsi) embedding = PartialEmbedding(infra, overlay, source_mapping=[(bso, nso)]) eso = ENode(bso, nso) esi = ENode(bsi, nsi) ein = ENode(bso, ni, bsi) print(embedding.possibilities()) assert embedding.take_action(eso, ein, 0) assert not embedding.take_action(ein, esi, 0)
def act(emb: PartialEmbedding, randomness, rand): """Take a semi-greedy action""" min_ts_actions = None possible_actions = sorted(emb.possibilities()) min_ts = inf for (u, v, t) in possible_actions: if t < min_ts: min_ts = t min_ts_actions = [] if t == min_ts: min_ts_actions.append((u, v, t)) preferred_actions = min_ts_actions not_relay_actions = [] for (u, v, t) in preferred_actions: if not v.relay: not_relay_actions.append((u, v, t)) if len(not_relay_actions) > 0: preferred_actions = not_relay_actions same_node_options = [] for (u, v, t) in preferred_actions: if u.node == v.node: same_node_options.append((u, v, t)) if len(same_node_options) > 0: preferred_actions = same_node_options # break out of reset loops by acting random every once in a while if rand.rand() < randomness: preferred_actions = possible_actions choice_idx = rand.choice(range(len(preferred_actions))) return preferred_actions[choice_idx]
def draw_embedding( embedding: PartialEmbedding, sources_color="red", sink_color="yellow", intermediates_color="green", ): """Draws a given PartialEmbedding""" g = embedding.graph shared_args = { "G": g, "node_size": 1000, "pos": nx.shell_layout(embedding.graph), } node_list = g.nodes() chosen = [node for node in node_list if g.nodes[node]["chosen"]] not_chosen = [node for node in node_list if not g.nodes[node]["chosen"]] def kind_color(node): kind = g.nodes[node]["kind"] color = intermediates_color if kind == "source": color = sources_color elif kind == "sink": color = sink_color return color nx.draw_networkx_nodes( nodelist=not_chosen, node_color=list(map(kind_color, not_chosen)), node_shape="o", **shared_args, ) nx.draw_networkx_nodes( nodelist=chosen, node_color=list(map(kind_color, chosen)), node_shape="s", **shared_args, ) nx.draw_networkx_labels(**shared_args) possibilities = embedding.possibilities() def chosen_color(edge): data = g.edges[edge] chosen = data["chosen"] (source, target, _) = edge if (source, target, data["timeslot"]) in possibilities: return "blue" if chosen: return "black" return "gray" def chosen_width(edge): data = g.edges[edge] (source, target, _) = edge chosen = data["chosen"] possible = (source, target, data["timeslot"]) in possibilities if chosen: return 2 if possible: return 1 return 0.1 edgelist = g.edges(keys=True) nx.draw_networkx_edges( **shared_args, edgelist=edgelist, edge_color=list(map(chosen_color, edgelist)), width=list(map(chosen_width, edgelist)), ) chosen_edges = [edge for edge in edgelist if g.edges[edge]["chosen"]] # Networkx doesn't really deal with drawing multigraphs very well. # Luckily for our presentation purposes its enough to pretend the # graph isn't a multigraph, so throw away the edge keys. labels = {(u, v): g.edges[(u, v, k)]["timeslot"] for (u, v, k) in chosen_edges} nx.draw_networkx_edge_labels(**shared_args, edgelist=chosen_edges, edge_labels=labels) timeslots = embedding.used_timeslots complete = embedding.is_complete() complete_str = " (complete)" if complete else "" plt.gca().text( -1, -1, f"{timeslots} timeslots{complete_str}", bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5), )