Commit 5d02a193 authored by Jarrod Pas's avatar Jarrod Pas
Browse files

Merge branch 'feature/trace-class' into 'develop'

Feature/trace class

See merge request !7
parents 66d7b883 068cae67
Pipeline #1650 passed with stage
in 1 minute and 31 seconds
...@@ -4,7 +4,7 @@ __author__ = "Jarrod Pas <j.pas@usask.ca>" ...@@ -4,7 +4,7 @@ __author__ = "Jarrod Pas <j.pas@usask.ca>"
from random import Random from random import Random
from pydtn import Network, Node, random_trace, RandomTraffic from pydtn import Network, Node, RandomTrace, RandomTraffic
class RandomNode(Node): class RandomNode(Node):
...@@ -53,7 +53,7 @@ def main(): ...@@ -53,7 +53,7 @@ def main():
trace_options = { trace_options = {
'seed': seed, 'seed': seed,
} }
trace = random_trace(nodes, **trace_options) trace = RandomTrace(nodes, **trace_options)
network = Network(nodes, traffic=traffic, trace=trace) network = Network(nodes, traffic=traffic, trace=trace)
network.run(until=duration) network.run(until=duration)
......
...@@ -9,7 +9,7 @@ from multiprocessing import Pool ...@@ -9,7 +9,7 @@ from multiprocessing import Pool
from pydtn import Network, RandomTraffic, Node, EpidemicNode from pydtn import Network, RandomTraffic, Node, EpidemicNode
from pydtn.community import BubbleNode, HCBFNode, LouvainCommunity from pydtn.community import BubbleNode, HCBFNode, LouvainCommunity
import pydtn.shed as shed from pydtn.shed import ShedTrace
Simulation = namedtuple('Simulation', ['trace', 'node_type', 'seed']) Simulation = namedtuple('Simulation', ['trace', 'node_type', 'seed'])
...@@ -19,8 +19,7 @@ def run_simulation(simulation): ...@@ -19,8 +19,7 @@ def run_simulation(simulation):
"""Run a simulation.""" """Run a simulation."""
seed = simulation.seed seed = simulation.seed
trace_metadata = shed.read_meta_file(simulation.trace) trace = ShedTrace(simulation.trace)
trace = shed.shed_trace(simulation.trace)
epoch = 7*24*60*60 # 7 days epoch = 7*24*60*60 # 7 days
...@@ -31,13 +30,13 @@ def run_simulation(simulation): ...@@ -31,13 +30,13 @@ def run_simulation(simulation):
} }
nodes = { nodes = {
node_id: simulation.node_type(**node_options) node_id: simulation.node_type(**node_options)
for node_id in range(trace_metadata['nodes']) for node_id in range(trace.nodes)
} }
traffic_options = { traffic_options = {
'seed': seed, 'seed': seed,
'start': epoch, 'start': epoch,
'speed': 30 * 60, # 1 packet every 30 mins 'step': 30 * 60, # 1 packet every 30 mins
} }
traffic = RandomTraffic(nodes, **traffic_options) traffic = RandomTraffic(nodes, **traffic_options)
...@@ -45,7 +44,7 @@ def run_simulation(simulation): ...@@ -45,7 +44,7 @@ def run_simulation(simulation):
network.run() network.run()
stats = { stats = {
'trace': simulation.trace, 'trace': trace.path,
'node_type': node_type.__name__, 'node_type': node_type.__name__,
'seed': seed, 'seed': seed,
} }
......
...@@ -19,8 +19,9 @@ __all__ = [ ...@@ -19,8 +19,9 @@ __all__ = [
'FloodingNode', 'FloodingNode',
'Contact', 'Contact',
'csv_trace', 'Trace',
'random_trace', 'CSVTrace',
'RandomTrace',
'Traffic', 'Traffic',
'RandomManyToManyTraffic', 'RandomManyToManyTraffic',
...@@ -540,31 +541,87 @@ class FloodingNode(Node): ...@@ -540,31 +541,87 @@ class FloodingNode(Node):
Contact = namedtuple('Contact', ['time', 'a', 'b', 'join']) Contact = namedtuple('Contact', ['time', 'a', 'b', 'join'])
def random_trace(nodes, seed=None, step=1): class Trace:
"""Generate a random contact trace.""" """Contact trace base class."""
random = Random(seed)
if isinstance(nodes, dict): def __init__(self, nodes):
nodes = list(nodes) """Create a contact trace."""
self._nodes = nodes
for now in count(step=step): @property
join = bool(random.getrandbits(1)) def nodes(self):
node_a, node_b = random.sample(nodes, 2) """Return number of nodes in trace."""
yield Contact(now, node_a, node_b, join) return self._nodes
def __iter__(self):
"""Yield no contacts."""
yield
@staticmethod
def create_contact(now, node_a, node_b, join):
"""Create a contact."""
return Contact(time=now, a=node_a, b=node_b, join=join)
class CSVTrace(Trace):
"""
Contact trace from csv file.
The first row is used as headers.
The second row is the following information about the trace:
[ int(duration), int(node_count), ignored, ignored ]
The csv uses the following columns:
[ int(time), int(node_a), int(node_b), int(join) ]
time -- simulation time that the contact even happens
node_{a,b} -- nodes involved in contact
join -- whether the contact is going up (1) or going down (0)
"""
def __init__(self, path):
"""Create a csv trace generator."""
with open(path) as csv_file:
csv_file = csv.reader(csv_file)
# skip header
next(csv_file)
# get node count from first row of csv. it is additional stats
# about the trace
# duration, placeholder, placehold, node_count
nodes = int(next(csv_file)[1])
super().__init__(nodes)
self.path = path
def __iter__(self):
"""Yield contacts from csv file."""
with open(self.path) as csv_file:
csv_file = csv.reader(csv_file)
# skip header and first row
next(csv_file)
next(csv_file)
for row in csv_file:
yield self.create_contact(*map(int, row))
class RandomTrace(Trace):
"""Contact trace from complete randomness."""
def __init__(self, nodes, seed=None, step=1):
"""Create a random trace generator."""
super().__init__(len(nodes))
self._node_list = list(nodes)
self.step = step
self.seed = seed
def __iter__(self):
"""Yield random trace."""
random = Random(self.seed)
def csv_trace(path): for now in count(step=self.step):
"""Generate contact trace from csv file.""" join = bool(random.getrandbits(1))
with open(path) as trace_file: node_a, node_b = random.sample(self._node_list, 2)
reader = csv.reader(trace_file) yield self.create_contact(now, node_a, node_b, join)
# skip header
next(reader)
# skip first line, it is additional stats about the trace:
# duration, placeholder, placehold, node_count
next(reader)
for row in reader:
now, source, destination, join = map(int, row)
yield Contact(now, source, destination, join)
class Traffic: class Traffic:
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
__all__ = [ __all__ = [
'write_meta_file', 'write_meta_file',
'read_meta_file', 'read_meta_file',
'shed_trace', 'ShedTrace',
] ]
__author__ = 'Jarrod Pas <j.pas@usask.ca>' __author__ = 'Jarrod Pas <j.pas@usask.ca>'
...@@ -15,7 +15,7 @@ from collections import defaultdict ...@@ -15,7 +15,7 @@ from collections import defaultdict
from itertools import groupby, count from itertools import groupby, count
from os import path from os import path
from pydtn import Contact from pydtn import Trace
def write_meta_file(meta_path, csv_path, duty_cycle_length=300): def write_meta_file(meta_path, csv_path, duty_cycle_length=300):
...@@ -56,53 +56,88 @@ def read_meta_file(meta_path): ...@@ -56,53 +56,88 @@ def read_meta_file(meta_path):
raise RuntimeError('Should not get here...') raise RuntimeError('Should not get here...')
def _get_contact_pairs(csv_path): class ShedTrace(Trace):
pairs = defaultdict(set) """Generator for contact traces from duty cycle based SHED datasets."""
with open(csv_path) as csv_file:
csv_file = csv.reader(csv_file) def __init__(self, meta_path):
next(csv_file) """
for row in csv_file: Create SHED trace generator.
_, source, _, target, _, slot = row
pair = min(source, target), max(source, target) Arguments:
slot = int(slot) meta_path -- path to a metadata file created with `write_meta_file`
pairs[pair].add(slot) """
return dict(pairs) self.path = meta_path
self.meta = read_meta_file(meta_path)
def shed_trace(meta_path): super().__init__(self.meta['nodes'])
"""
Generate contact trace for a duty cycle based SHED dataset. self._pairs = None
self._contacts = None
Keyword Arguments:
duty_cycle_length -- duration of each duty cycle (default 300) @property
""" def contact_pairs(self):
meta = read_meta_file(meta_path) """Return all pairs and their duty cycles that they are in contact."""
pairs = _get_contact_pairs(meta['data']) if self._pairs is not None:
node = count() return self._pairs
nodes = {}
contacts = [] pairs = defaultdict(set)
for (source, target), slots in pairs.items(): with open(self.meta['data']) as csv_file:
# get canonical node id for source csv_file = csv.reader(csv_file)
if source not in nodes: next(csv_file)
nodes[source] = next(node) for row in csv_file:
source = nodes[source] _, source, _, target, _, slot = row
pair = min(source, target), max(source, target)
# get canonical node id for source slot = int(slot)
if target not in nodes: pairs[pair].add(slot)
nodes[target] = next(node)
target = nodes[target] self._pairs = dict(pairs)
return self._pairs
slots = sorted(slots)
@property
# groups consecutive slots def contacts(self):
# if the lambda is mapped it will return: """
# [1, 2, 3, 6, 7, 9] -> [-1, -1, -1, -3, -3, -4] Calculate contacts in the SHED dataset.
for _, group in groupby(enumerate(slots), lambda p: p[0]-p[1]):
times = list(map(lambda g: g[1], group)) Computes contacts by finding consecutive duty cycles that any give pair
start = times[0] * meta['duty_cycle_length'] of nodes saw each other. The end of each contact is at the start of
end = (times[-1] + 1) * meta['duty_cycle_length'] the duty cycle that they did not see each other.
"""
contacts.append(Contact(start, source, target, True)) if self._contacts is not None:
contacts.append(Contact(end, source, target, False)) return self._contacts
yield from sorted(contacts) node = count()
nodes = {}
contacts = []
for (source, target), slots in self.contact_pairs.items():
# get canonical node id for source
if source not in nodes:
nodes[source] = next(node)
source = nodes[source]
# get canonical node id for source
if target not in nodes:
nodes[target] = next(node)
target = nodes[target]
slots = sorted(slots)
# groups consecutive slots
# if the lambda is mapped it will return:
# [1, 2, 3, 6, 7, 9] -> [-1, -1, -1, -3, -3, -4]
for _, group in groupby(enumerate(slots), lambda p: p[0]-p[1]):
times = list(map(lambda g: g[1], group))
start = times[0] * self.meta['duty_cycle_length']
end = (times[-1] + 1) * self.meta['duty_cycle_length']
contacts.extend([
self.create_contact(start, source, target, True),
self.create_contact(end, source, target, False),
])
contacts.sort()
self._contacts = contacts
return self._contacts
def __iter__(self):
"""Yield contacts in SHED dataset."""
yield from self.contacts
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment