-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinfrastructure.py
276 lines (234 loc) · 8.59 KB
/
infrastructure.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""Modelling the physical network"""
from typing import FrozenSet
from functools import lru_cache
from enum import Enum
from math import inf
import numpy as np
import networkx as nx
import wsignal
class NodeKind(Enum):
"""Types of infrastructure nodes"""
source = 1
sink = 2
intermediate = 3
class InfrastructureNetwork:
"""Model of the physical network"""
# pylint: disable=too-many-instance-attributes
# Instance attributes needed for caching, I think private instance
# attributes are fine.
def __init__(self, bandwidth=1, noise_floor_dbm: float = -30):
self._last_id = 0
# Link capacity is influenced by the SINR and the bandwidth.
# Leaving the bandwidth set to 1 will result in a link capacity
# that is relative to the actual bandwidth (so capacity
# requirements are in the format of
# "b bits per second per bandwidth"
self.bandwidth = bandwidth
# https://www.quora.com/How-high-is-the-ambient-RF-noise-floor-in-the-2-4-GHz-spectrum-in-downtown-San-Francisco
self.noise_floor_dbm = noise_floor_dbm
self.graph = nx.Graph()
self.sink = None
self.sources = set()
self.intermediates = set()
# transparent caching per instance
self.power_at_node = self._power_at_node
self.sinr = lru_cache(1)(self._sinr)
self.power_at_node = lru_cache(1)(self._power_at_node)
self.power_received_dbm = lru_cache(None)(self._power_received_dbm)
def _reset_caches(self):
nodes = len(self.nodes())
if hasattr(self, "sinr"):
self.sinr.cache_clear()
# Enough space for all pairwise SINRs for 20 different
# configurations of sending nodes. Most relevant is the "no
# sending nodes" case, which will happen all the time.
sinr_maxsize = min(20 * nodes ** 2, 10 * 1024) # upper bound
self.sinr = lru_cache(maxsize=sinr_maxsize)(self._sinr)
if hasattr(self, "power_at_node"):
self.power_at_node.cache_clear()
self.power_at_node = lru_cache(maxsize=100 * nodes)(
self._power_at_node
)
if hasattr(self, "power_received_dbm"):
self.power_received_dbm.cache_clear()
self.power_received_dbm = lru_cache(maxsize=nodes)(
self._power_received_dbm
)
def __getstate__(self):
state = self.__dict__.copy()
# don't pickle caches
del state["sinr"]
del state["power_at_node"]
del state["power_received_dbm"]
return state
def __setstate__(self, state):
self.__dict__.update(state)
self._reset_caches()
def nodes(self):
"""Returns all infrastructure nodes"""
return self.graph.nodes()
def add_intermediate(
self,
pos: (float, float),
transmit_power_dbm: float,
capacity: float = inf,
name: str = None,
):
"""Adds an intermediate node to the infrastructure graph"""
node = self._add_node(
pos, transmit_power_dbm, NodeKind.intermediate, capacity, name
)
self.intermediates.add(node)
return node
def add_source(
self,
pos: (float, float),
transmit_power_dbm: float,
capacity: float = inf,
name: str = None,
):
"""Adds a source node to the infrastructure graph"""
node = self._add_node(
pos, transmit_power_dbm, NodeKind.source, capacity, name
)
self.sources.add(node)
return node
def set_sink(
self,
pos: (float, float),
transmit_power_dbm: float,
capacity: float = inf,
name=None,
):
"""Sets the node to the infrastructure graph"""
node = self._add_node(
pos, transmit_power_dbm, NodeKind.sink, capacity, name
)
self.sink = node
return node
def _add_node(
self,
pos: (float, float),
transmit_power_dbm: float,
kind: NodeKind,
capacity: float,
name: str = None,
):
if name is None:
name = self._generate_name()
self.graph.add_node(
name,
kind=kind,
pos=pos,
capacity=capacity,
transmit_power_dbm=transmit_power_dbm,
)
self._reset_caches()
return name
def capacity(self, node):
"""Returns the capacity of a given node"""
return self.graph.node[node]["capacity"]
def position(self, node):
"""Returns the position of a given node"""
return self.graph.node[node]["pos"]
def power(self, node):
"""Returns the transmit power of a given node"""
return self.graph.node[node]["transmit_power_dbm"]
def min_node_distance(self):
"""Calculates the distance between the closest nodes"""
min_distance = inf
for a in self.nodes():
x1, y1 = self.position(a)
for b in self.nodes():
if b == a:
continue
x2, y2 = self.position(b)
dist = wsignal.distance(x1, y1, x2, y2)
if dist < min_distance:
min_distance = dist
return min_distance
def _power_received_dbm(self, source, target):
"""Power received at sink if source sends at full power"""
source_node = self.graph.nodes[source]
target_node = self.graph.nodes[target]
src_x, src_y = source_node["pos"]
trg_x, trg_y = target_node["pos"]
distance = wsignal.distance(src_x, src_y, trg_x, trg_y)
transmit_power_dbm = source_node["transmit_power_dbm"]
return wsignal.power_received(distance, transmit_power_dbm)
def _power_at_node(self, node: str, senders: FrozenSet[str]):
"""Calculates the amount of power a node receives (signal+noise)
assuming only `senders` sends"""
# We need to convert to watts for addition (log scale can only
# multiply)
received_power_watt = 0
for sender in senders:
p_r = self.power_received_dbm(sender, node)
received_power_watt += wsignal.dbm_to_watt(p_r)
return wsignal.watt_to_dbm(received_power_watt)
def _sinr(self, source: str, target: str, senders: FrozenSet[str]):
"""
SINR assuming only `senders` are sending.
"""
received_signal_dbm = self.power_received_dbm(source, target)
# everything already sending is assumed to be interference
received_interference_dbm = self.power_at_node(target, senders=senders)
return wsignal.sinr(
received_signal_dbm,
received_interference_dbm,
self.noise_floor_dbm,
)
def _generate_name(self):
self._last_id += 1
return f"N{self._last_id}"
def __str__(self):
result = "infra = InfrastructureNetwork():\n"
for source in self.sources:
s = self._node_to_verbose_str(source)
result += f"{source} = infra.add_source{s}\n"
for intermediate in self.intermediates:
i = self._node_to_verbose_str(intermediate)
result += f"{intermediate} = infra.add_intermediate{i}\n"
s = self._node_to_verbose_str(self.sink)
result += f"{self.sink} = infra.set_sink{s}\n"
return result
def _node_to_verbose_str(self, node):
pos = self.graph.nodes[node]["pos"]
pos = f"({round(pos[0], 1)}, {round(pos[1], 1)})"
tp = round(self.graph.nodes[node]["transmit_power_dbm"], 1)
cap = round(self.capacity(node), 1)
return (
f'(name="{node}", '
f"pos={pos}, "
f"transmit_power_dbm={tp}, "
f"capacity={cap})"
)
def draw_infra(
infra: InfrastructureNetwork,
sources_color="red",
sink_color="yellow",
intermediates_color="green",
):
"""Draws a given InfrastructureNetwork"""
shared_args = {
"G": infra.graph,
"pos": nx.get_node_attributes(infra.graph, "pos"),
"node_size": 450,
}
nx.draw_networkx_nodes(
nodelist=list(infra.sources), node_color=sources_color, **shared_args
)
nx.draw_networkx_nodes(
nodelist=list(infra.intermediates),
node_color=intermediates_color,
**shared_args,
)
nx.draw_networkx_nodes(
nodelist=[infra.sink], node_color=sink_color, **shared_args
)
nx.draw_networkx_labels(**shared_args)
if __name__ == "__main__":
from generator import DefaultGenerator
draw_infra(DefaultGenerator().random_infrastructure(2, rand=np.random))
from matplotlib import pyplot as plt
plt.show()