From ab8a1ae756f1ff9c3b03d323cc5e01fb6b603ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20C=CC=A7ak=C4=B1r?= Date: Mon, 31 Dec 2018 14:46:37 +0300 Subject: [PATCH] Add CmpE49F and CmpE487 Final project --- .gitignore | 2 - cmpe487/final-project | 1 + cmpe49f/cache/.gitignore | 1 + cmpe49f/cache/README.md | 18 +++ cmpe49f/cache/config.py | 37 +++++ cmpe49f/cache/graph.py | 51 ++++++ cmpe49f/cache/main.py | 68 ++++++++ cmpe49f/cache/models/cache.py | 33 ++++ cmpe49f/cache/models/content.py | 32 ++++ cmpe49f/cache/models/log.py | 75 +++++++++ cmpe49f/cache/models/network.py | 66 ++++++++ cmpe49f/cache/models/simulation.py | 251 +++++++++++++++++++++++++++++ cmpe49f/cache/models/user.py | 207 ++++++++++++++++++++++++ cmpe49f/cache/utils.py | 16 ++ 14 files changed, 856 insertions(+), 2 deletions(-) create mode 160000 cmpe487/final-project create mode 100644 cmpe49f/cache/.gitignore create mode 100644 cmpe49f/cache/README.md create mode 100644 cmpe49f/cache/config.py create mode 100644 cmpe49f/cache/graph.py create mode 100644 cmpe49f/cache/main.py create mode 100644 cmpe49f/cache/models/cache.py create mode 100644 cmpe49f/cache/models/content.py create mode 100644 cmpe49f/cache/models/log.py create mode 100644 cmpe49f/cache/models/network.py create mode 100644 cmpe49f/cache/models/simulation.py create mode 100644 cmpe49f/cache/models/user.py create mode 100644 cmpe49f/cache/utils.py diff --git a/.gitignore b/.gitignore index 3df04ca..10508e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ cmpe443 -cmpe487/final-project -cmpe49f/ diff --git a/cmpe487/final-project b/cmpe487/final-project new file mode 160000 index 0000000..d3a72a4 --- /dev/null +++ b/cmpe487/final-project @@ -0,0 +1 @@ +Subproject commit d3a72a40a18780d2764be61e05a08cfb3495abf6 diff --git a/cmpe49f/cache/.gitignore b/cmpe49f/cache/.gitignore new file mode 100644 index 0000000..89547ba --- /dev/null +++ b/cmpe49f/cache/.gitignore @@ -0,0 +1 @@ +log.txt \ No newline at end of file diff --git a/cmpe49f/cache/README.md b/cmpe49f/cache/README.md new file mode 100644 index 0000000..0989993 --- /dev/null +++ b/cmpe49f/cache/README.md @@ -0,0 +1,18 @@ +## CmpE 49F +### Caching Project + +### Requirements +- python 3 +- simpy +- numpy +- matplotlib + +### Usage +```bash + $ python3 main.py + # You can overwrite time, count, and algorithm via command arguments + $ python3 main.py --time=2000 --count=1 --algorithm=MY + # Algorithm parameter has 4 different options: LRU | LFU | RAND | MY +``` + +You can set general system parameters at `config.py` diff --git a/cmpe49f/cache/config.py b/cmpe49f/cache/config.py new file mode 100644 index 0000000..1124c7c --- /dev/null +++ b/cmpe49f/cache/config.py @@ -0,0 +1,37 @@ +import math + +# GENERAL +ALGORITHM = "LRU" # LRU | LFU | RAND | MY +SIMULATION_TIME = 100 +SEED = 10 +NUMBER_OF_SIMULATIONS = 10 + +# CONTENT +NUMBER_OF_CONTENTS = 100 +ZIPF_PARAMETER = 0.8 +LAMBDA_BASE_SIZE = 25e6 +LAMBDA_ENC_SIZE = 5e6 +PROBABILITY_HQ = 0.5 + +# USER +LAMBDA_USERS_PPP = 0.0015 +LAMBDA_PRIMARY_USER = 1 +LAMBDA_SECONDARY_USER = 1.5 +PRIMARY_USER_DISTANCE = 150 +CACHE_CAPACITY = 1e9 +NUMBER_OF_CACHE_TRY = 5 + +# ENV +RADIUS = 300 +DEVICE_RADIUS = math.sqrt((NUMBER_OF_CONTENTS / LAMBDA_USERS_PPP) / math.pi) + +# NETWORK +NUMBER_OF_CHANNELS = 10 +CHANNEL_BANDWIDTH = 2e6 +LAMBDA_PRIMARY_CHANNEL = LAMBDA_PRIMARY_USER / NUMBER_OF_CHANNELS +INITIAL_FREQUENCY = 700e6 +NOISE = 1.6e-19 + +# PERFORMANCE +BASE_LATENCY = 0.25 +ENC_LATENCY = 0.05 diff --git a/cmpe49f/cache/graph.py b/cmpe49f/cache/graph.py new file mode 100644 index 0000000..d732edb --- /dev/null +++ b/cmpe49f/cache/graph.py @@ -0,0 +1,51 @@ +import matplotlib.pyplot as plt +import matplotlib.patches as mpatch + + +def draw_plot(logs, sim_time): + fig, ax = plt.subplots() + data = [] + for i in range(10): + data.append({"x": [], "color": [], "border": []}) + + for log in logs: + if log.ev_type == "SERVED" and log.user == "PU": + data[log.freq]['x'].append((log.start, log.end - log.start)) + data[log.freq]['color'].append('#7db1fc') + data[log.freq]['border'].append('#004c99') + elif log.ev_type == "SERVED" and log.user == "SU" and log.layer == "BASE": + data[log.freq]['x'].append((log.start, log.end - log.start)) + data[log.freq]['color'].append('#a8e2a5') + data[log.freq]['border'].append('#24a624') + + elif log.ev_type == "SERVED" and log.user == "SU" and log.layer == "ENH": + data[log.freq]['x'].append((log.start, log.end - log.start)) + data[log.freq]['color'].append('#f99f9f') + data[log.freq]['border'].append('#bc48aa') + + elif log.ev_type == "DROPPED" and log.user == "SU" and log.layer == "BASE": + data[log.freq]['x'].append((log.start, log.end - log.start)) + data[log.freq]['color'].append('#fcd1a4') + data[log.freq]['border'].append('#e1b43f') + + elif log.ev_type == "DROPPED" and log.user == "SU" and log.layer == "ENH": + data[log.freq]['x'].append((log.start, log.end - log.start)) + data[log.freq]['color'].append('#f86161') + data[log.freq]['border'].append('#c50c0c') + + for i, channel in enumerate(data): + ax.broken_barh(channel['x'], (700 + i * 2, 2), facecolors=channel['color'], edgecolors=channel['border']) + + ax.set_ylim(700, 720) + ax.set_xlim(0, sim_time) + ax.set_xlabel('Channel history') + ax.set_yticks([702 + 2 * i for i in range(10)]) + ax.set_yticklabels(["f{}".format(i + 1) for i in range(10)]) + ax.grid(True) + ax.legend([mpatch.Rectangle((0, 0), 1, 1, fc="#7db1fc"), + mpatch.Rectangle((0, 0), 1, 1, fc="#a8e2a5"), + mpatch.Rectangle((0, 0), 1, 1, fc="#f99f9f"), + mpatch.Rectangle((0, 0), 1, 1, fc="#fcd1a4"), + mpatch.Rectangle((0, 0), 1, 1, fc="#f86161")], + ['PU', 'SU-BASE', 'SU-ENH', 'SU-BASE Drop', 'SU-ENH Drop']) + plt.show() diff --git a/cmpe49f/cache/main.py b/cmpe49f/cache/main.py new file mode 100644 index 0000000..d5b6e28 --- /dev/null +++ b/cmpe49f/cache/main.py @@ -0,0 +1,68 @@ +from models.simulation import Simulation +import config +# from config import SEED, SIMULATION_TIME, NUMBER_OF_SIMULATIONS, ALGORITHM +import random +import argparse +from utils import * +import os +from threading import Thread +import time +from graph import * + + +def print_value(name, value): + print("\t{} {}".format(change_style(name + ":", 'green').rjust(44), change_style(value, 'bold'))) + + +# Parse arguments +parser = argparse.ArgumentParser() +parser.add_argument('--time', help='simulation time', dest='time', type=int, default=config.SIMULATION_TIME) +parser.add_argument('--count', help='how many time simulation runs', dest='count', type=int, + default=config.NUMBER_OF_SIMULATIONS) +parser.add_argument('--seed', help='random seed', dest='seed', type=int, default=config.SEED) +parser.add_argument('--algorithm', help='cache replacement algorithm: LRU, LFU, RAND, MY', dest='algorithm', + default=config.ALGORITHM) +args = parser.parse_args() +config.SIMULATION_TIME = args.time +config.NUMBER_OF_SIMULATIONS = args.count +config.SEED = args.seed +config.ALGORITHM = args.algorithm + +# Delete old logs +os.system("rm -f log_*.txt") + +# Start simulations +start_time = time.time() +if config.NUMBER_OF_SIMULATIONS == 1: + sim = Simulation(config.SEED, config.SIMULATION_TIME, 1) + sim.start() + draw_plot(sim.logger.logs, config.SIMULATION_TIME) +else: + threads = [] + + for i in range(config.NUMBER_OF_SIMULATIONS): + sim = Simulation(random.randint(1, 9999), config.SIMULATION_TIME, i + 1) + thread = Thread(target=sim.start) + thread.start() + threads.append(thread) + + for t in threads: + t.join() + +end_time = time.time() + +os.system("clear") +print("\n\n" + change_style("=== PERFORMANCE RESULTS ===\n", "blue").rjust(64)) +print_value("ALGORITHM", config.ALGORITHM) +print_value("SIMULATION TIME", config.SIMULATION_TIME) +print_value("SIMULATION COUNT", config.NUMBER_OF_SIMULATIONS) +print_value("REAL TIME", "{:.5f} seconds".format(end_time - start_time)) +print_value("AVERAGE LATENCY", "{:.5f}".format(Simulation.performances['latency'] / config.NUMBER_OF_SIMULATIONS)) +print_value("LOCAL HIT RATE SQ", "{:.5f}".format(Simulation.performances['p']['sq'] / config.NUMBER_OF_SIMULATIONS)) +print_value("LOCAL HIT RATE HQ BASE", + "{:.5f}".format(Simulation.performances['p']['hq']['base'] / config.NUMBER_OF_SIMULATIONS)) +print_value("LOCAL HIT RATE HQ ENH. | BASE LH", + "{:.5f}".format(Simulation.performances['p']['hq']['enh']['base_local_hit'] / config.NUMBER_OF_SIMULATIONS)) +print_value("LOCAL HIT RATE HQ ENH. | BASE D2D", + "{:.5f}".format(Simulation.performances['p']['hq']['enh']['base_d2d'] / config.NUMBER_OF_SIMULATIONS)) +print("\n\n") diff --git a/cmpe49f/cache/models/cache.py b/cmpe49f/cache/models/cache.py new file mode 100644 index 0000000..5b92b9d --- /dev/null +++ b/cmpe49f/cache/models/cache.py @@ -0,0 +1,33 @@ +from enum import Enum +from models.content import Content +import random + + +class CacheType(Enum): + """ Represents type of cache """ + BASE = "Base" + ENHANCEMENT = "Enh" + + +class Cache: + """ Represents content cache at device cache storage """ + + def __init__(self, id, type, size): + self.id = id + self.type = type + self.size = size + self.LFU = 0 + self.LRU = 0 + self.weight = 0 + + def __str__(self): + return "Cache" + "_" + str(self.type.value) + '_' + str(self.id) + + @staticmethod + def get_random(contents): + """ Return random cache for initial filling""" + content = Content.get_random(contents) + if random.random() < .5: + return Cache(content.id, CacheType.ENHANCEMENT, content.enhancement) + else: + return Cache(content.id, CacheType.BASE, content.base) diff --git a/cmpe49f/cache/models/content.py b/cmpe49f/cache/models/content.py new file mode 100644 index 0000000..7a4e3f0 --- /dev/null +++ b/cmpe49f/cache/models/content.py @@ -0,0 +1,32 @@ +from config import ZIPF_PARAMETER, NUMBER_OF_CONTENTS, LAMBDA_BASE_SIZE, LAMBDA_ENC_SIZE +import numpy as np +import random + + +class Content: + """ Represents content in simulation """ + + def __init__(self, id, base, enhancement): + self.id = id + self.base = base + self.enhancement = enhancement + self.popularity = self.calculate_popularity() + + def calculate_popularity(self): + return (1 / (self.id ** ZIPF_PARAMETER)) / sum( + [(1 / (n ** ZIPF_PARAMETER)) for n in range(1, NUMBER_OF_CONTENTS + 1)]) + + @staticmethod + def get_random(contents): + """ Returns random content for secondary user request """ + return np.random.choice(contents, p=[content.popularity for content in contents]) + + @staticmethod + def generate(): + """ Generates initial contents """ + contents = [] + for i in range(1, NUMBER_OF_CONTENTS + 1): + base = random.expovariate(1 / LAMBDA_BASE_SIZE) + enhancement = random.expovariate(1 / LAMBDA_ENC_SIZE) + contents.append(Content(i, int(base), int(enhancement))) + return contents diff --git a/cmpe49f/cache/models/log.py b/cmpe49f/cache/models/log.py new file mode 100644 index 0000000..ea7a181 --- /dev/null +++ b/cmpe49f/cache/models/log.py @@ -0,0 +1,75 @@ +class Logger: + """ Keeps logs of the simulation """ + + def __init__(self, seed): + self.seed = seed + self.logs = [] + self.id_counter = 1 + + def new(self, ev_type, is_hq, user, layer, start, end, tx_device, rec_device, freq, prev_ev_id=None): + """ Creates new log """ + log = Log(self.id_counter, ev_type, "HQ" if is_hq else "SQ", user, layer, start, end, tx_device, rec_device, + freq, prev_ev_id) + self.logs.append(log) + self.id_counter += 1 + + return log.id + + def save(self): + """ Writes logs to file """ + file = open("log_" + str(self.seed) + ".txt", 'w') + header = "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |".format("EV ID".ljust(5), + "PV ID".ljust(5), + "EV TYPE".ljust(12), + "REQ TYPE".ljust(8), + "USE".ljust(3), + "LAYER".ljust(5), + "START TIME".ljust(12), + "END TIME".ljust(12), + "TX DEVICE".ljust(10), + "REC DEVICE".ljust(10), + "FREQUENCY".ljust(10)) + header += "\n" + ("-" * 126) + file.write(header + "\n") + # for log in sorted(Log.logs, key=lambda k: k.start): + for log in self.logs: + file.write(str(log) + "\n") + header += ("-" * 126) + + +class Log: + """ Represents log record for calculations """ + + def __init__(self, id, ev_type, req_type, user, layer, start, end, tx_device, rec_device, freq, prev_ev_id=None): + self.id = id + self.ev_type = ev_type + self.req_type = req_type + self.user = user + self.layer = layer + self.start = start + self.end = end + self.tx_device = tx_device + self.rec_device = rec_device + self.freq = freq + self.prev_ev_id = prev_ev_id if prev_ev_id else self.id + + def __str__(self): + return "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |".format(str(self.id).ljust(5), + str(self.prev_ev_id).ljust(5), + self.ev_type.ljust(12), + self.req_type.ljust(8), + self.user.ljust(3), + self.layer.ljust(5), + "{:.2f} sec".format(self.start).ljust( + 12), + "{:.2f} sec".format(self.end).ljust( + 12), + ( + "NON" if self.tx_device is None else "dev_{}".format( + self.tx_device)).ljust(10), + ( + "NON" if self.rec_device is None else "dev_{}".format( + self.rec_device)).ljust(10), + ( + "NON" if self.freq is None else "f_{}".format( + self.freq + 1)).ljust(10)) diff --git a/cmpe49f/cache/models/network.py b/cmpe49f/cache/models/network.py new file mode 100644 index 0000000..71fd4d3 --- /dev/null +++ b/cmpe49f/cache/models/network.py @@ -0,0 +1,66 @@ +from simpy import PreemptiveResource, Interrupt +from config import NUMBER_OF_CHANNELS, CHANNEL_BANDWIDTH, INITIAL_FREQUENCY +import random + + +class Network: + """ Handles channel allocation operations """ + + def __init__(self, env): + self.env = env + self.channels = [PreemptiveResource(env, capacity=1) for _ in range(NUMBER_OF_CHANNELS)] + + def serve(self, user, cache, distance): + """ Serves channel to user for transferring content """ + service_time = user.get_service_time(cache, distance) + request = self.channels[user.channel_id].request(priority=user.get_priority(), preempt=True) + request.user = user + success = False + yield request + try: + user.serving = True + user.print("Start serving f_" + str(user.channel_id + 1)) + yield self.env.timeout(service_time) + success = True + except Interrupt: + user.print("Drop serving f_" + str(user.channel_id + 1), 'red') + finally: + self.channels[user.channel_id].release(request) + user.serving = False + if success: + user.print("End serving f_" + str(user.channel_id + 1)) + + return success + + def get_current_type(self, ch): + """ Returns type of current user that allocates given channel""" + + user = self.get_current_user(ch) + if user: + return user.type + return None + + def get_current_user(self, ch): + """ Returns current user that allocates given channel""" + channel = self.channels[ch] + if channel.users: + return channel.users[0].user + + return None + + def get_idle_channel(self): + """ Returns first idle channel """ + idles = [] + for ch in range(NUMBER_OF_CHANNELS): + if self.channels[ch].count == 0: + idles.append(ch) + + if idles: + return random.choice(idles) + + return None + + @staticmethod + def get_channel_frequency(ch): + """ Calculates frequency of given channel """ + return INITIAL_FREQUENCY + (int(ch) - 1) * CHANNEL_BANDWIDTH diff --git a/cmpe49f/cache/models/simulation.py b/cmpe49f/cache/models/simulation.py new file mode 100644 index 0000000..ed5ff3b --- /dev/null +++ b/cmpe49f/cache/models/simulation.py @@ -0,0 +1,251 @@ +import simpy +from models.user import UserType +from models.user import User +from models.user import Point +from models.cache import CacheType +from models.cache import Cache +from models.content import Content +from models.network import Network +from models.log import Logger +from threading import Lock, Semaphore +import random +from utils import * +from config import NUMBER_OF_CHANNELS, PRIMARY_USER_DISTANCE, PROBABILITY_HQ, LAMBDA_PRIMARY_USER, \ + LAMBDA_SECONDARY_USER, BASE_LATENCY, ENC_LATENCY + + +class Simulation: + """ Manages simulation's environment """ + performances = {"latency": 0, "p": {"sq": 0, "hq": {"base": 0, "enh": {"base_local_hit": 0, "base_d2d": 0}}}} + lock = Lock() + semaphore = Semaphore(20) + + def __init__(self, seed, time, id): + self.id = id + self.seed = seed + self.time = time + self.network = None + self.env = None + self.logger = Logger(seed) + self.users = [] + self.contents = [] + + def handle_primary_user(self, user): + start_time = self.env.now + # Choose a random frequency channel + channel = random.choice(range(NUMBER_OF_CHANNELS)) + + # Get user that using this channel at the moment + current_user = self.network.get_current_user(channel) + if current_user is None: + # Channel is idle, serve PU + user.channel_id = channel + content = Content.get_random(self.contents) + yield from self.network.serve(user, Cache(content.id, CacheType.BASE, 25e6), + PRIMARY_USER_DISTANCE) + self.logger.new("SERVED", False, "PU", "BASE", start_time, self.env.now, None, None, channel) + elif current_user.type == UserType.PRIMARY: + # Channel is used by another PU, block coming PU + user.print("Block f_" + str(channel + 1), 'red') + self.logger.new("BLOCKED", False, "PU", "BASE", start_time, self.env.now, None, None, None) + elif current_user.type == UserType.SECONDARY: + # Channel is used by SU, drop SU, serve PU + user.channel_id = channel + content = Content.get_random(self.contents) + user.print("Preempt f_" + str(channel + 1), 'blue') + yield from self.network.serve(user, Cache(content.id, CacheType.BASE, 25e6), + PRIMARY_USER_DISTANCE) + self.logger.new("SERVED", False, "PU", "BASE", start_time, self.env.now, None, None, channel) + + def handle_secondary_user(self, user): + start_time = self.env.now + # Check if it's already in system + if user.serving: + user.print("Blocked already serving", 'red') + self.logger.new("BLOCKED", False, "SU", "BASE", start_time, self.env.now, None, None, None) + return + # Get idle channel for SU + idle_channel = self.network.get_idle_channel() + + if idle_channel is not None: + # We found idle channel + content = Content.get_random(self.contents) + user.channel_id = idle_channel + + success = True + blocked = False + is_hq = random.random() < PROBABILITY_HQ + prev_event_id = None + + # Create cache instance from random content + cache_base = Cache(content.id, CacheType.BASE, content.base) + if user.is_cached(cache_base): + # Has base layer of content at our device + user.print("Local hit " + str(cache_base), 'green') + user.used_cache(cache_base) # Change LFU and LRU values + prev_event_id = self.logger.new("LOCAL HIT", is_hq, "SU", "BASE", start_time, self.env.now, user.id, + user.id, + None) + else: + # Looks for base layer to other users + source = user.look_other_users(cache_base, self.users) + if source is None: + self.logger.new("BLOCKED", is_hq, "SU", "BASE", start_time, self.env.now, None, None, None) + user.print("Not find cache " + str(cache_base), 'red') + success = False + blocked = True + else: + user.print("Found " + str(cache_base) + " at " + str(source), 'blue') + success = yield from self.network.serve(user, cache_base, + user.position.distance(source.position)) + prev_event_id = self.logger.new("SERVED" if success else "DROPPED", is_hq, "SU", "BASE", + start_time, + self.env.now, + source.id, user.id, user.channel_id) + if success: + user.store_cache(cache_base, self.users, self.contents) + if is_hq: + # Look for enh layer after base is finished + start_time = self.env.now + if success: + # Download base layer successfully + cache_enhancement = Cache(content.id, CacheType.ENHANCEMENT, content.enhancement) + if user.is_cached(cache_enhancement): + # Has enh layer of content at our device + user.print("Local hit " + str(cache_enhancement), 'green') + self.logger.new("LOCAL HIT", is_hq, "SU", "ENH", start_time, self.env.now, user.id, user.id, + None, + prev_event_id) + else: + source = user.look_other_users(cache_enhancement, self.users) + if source is None: + self.logger.new("BLOCKED", is_hq, "SU", "ENH", start_time, self.env.now, None, None, + None) + user.print("Not find cache " + str(cache_enhancement), 'red') + else: + user.print("Found " + str(cache_enhancement) + " at " + str(source), 'blue') + + success = yield from self.network.serve(user, cache_enhancement, + user.position.distance(source.position)) + self.logger.new("SERVED" if success else "DROPPED", is_hq, "SU", "ENH", start_time, + self.env.now, + source.id, user.id, user.channel_id, prev_event_id) + user.store_cache(cache_enhancement, self.users, self.contents) + + else: + # Couldn't download base layer successfully + self.logger.new("BLOCKED" if blocked else "DROPPED", is_hq, "SU", "ENH", start_time, + self.env.now, + None, user.id, user.channel_id, prev_event_id) + else: + # We couldn't find idle channel, block coming SU + user.print("No idle channel", 'red') + self.logger.new("BLOCKED", False, "SU", "BASE", start_time, self.env.now, None, None, None) + + def request_content(self, user): + if user.type == UserType.PRIMARY: + yield from self.handle_primary_user(user) + elif user.type == UserType.SECONDARY: + yield from self.handle_secondary_user(user) + + def calculate_performance(self): + t_all = 0 + count = {"base": 0, "enh": 0} + count_sq = {"total": 0, "local_hit": 0} + count_hq = {"base": {"total": 0, "local_hit": 0, "d2d": 0}, "enh": {"base_local_hit": 0, "base_d2d": 0}} + hq_base_local_hits = [] + hq_base_d2d = [] + for log in self.logger.logs: + if log.user == "SU": + if log.req_type == "SQ": + count_sq["total"] += 1 + if log.ev_type == "SERVED": + t_all += (log.end - log.start) + count["base"] += 1 + elif log.ev_type == "LOCAL HIT": + t_all += BASE_LATENCY + count["base"] += 1 + count_sq["local_hit"] += 1 + elif log.req_type == "HQ" and log.layer == "BASE": + count_hq['base']['total'] += 1 + if log.ev_type == "SERVED": + t_all += (log.end - log.start) + count["enh"] += 1 + count_hq['base']['d2d'] += 1 + hq_base_d2d.append(log.id) + elif log.ev_type == "LOCAL HIT": + t_all += BASE_LATENCY + count["enh"] += 1 + count_hq['base']['local_hit'] += 1 + hq_base_local_hits.append(log.id) + elif log.req_type == "HQ" and log.layer == "ENH": + if log.ev_type == "SERVED": + t_all += (log.end - log.start) + elif log.ev_type == "LOCAL HIT": + t_all += ENC_LATENCY + if log.prev_ev_id in hq_base_local_hits: + count_hq['enh']['base_local_hit'] += 1 + elif log.prev_ev_id in hq_base_d2d: + count_hq['enh']['base_d2d'] += 1 + + latency = t_all / (count["base"] + count["enh"]) + p_loc_sq = count_sq["local_hit"] / count_sq["total"] + p_loc_hq_base = count_hq['base']['local_hit'] / count_hq['base']['total'] + p_loc_hq_enh_base_loc = count_hq['enh']['base_local_hit'] / count_hq['base']['local_hit'] + p_loc_hq_enh_base_d2d = count_hq['enh']['base_d2d'] / count_hq['base']['d2d'] + + return {"latency": latency, "p": {"sq": p_loc_sq, "hq": {"base": p_loc_hq_base, + "enh": {"base_local_hit": p_loc_hq_enh_base_loc, + "base_d2d": p_loc_hq_enh_base_d2d}}}} + + def arrival_process(self, arrival_rate, type): + i = 0 + while True: + interval = random.expovariate(arrival_rate) + yield self.env.timeout(interval) + if type == UserType.PRIMARY: + user = User(i, UserType.PRIMARY, Point(0, 0), self.env, self.contents) + i += 1 + else: + user = User.get_random(self.users) # Get a random user from generated secondary users + + user.print("Arrived", 'green') + self.env.process(self.request_content(user)) + + def start(self): + """ Runs simulation with given seed""" + Simulation.semaphore.acquire() + self.env = simpy.Environment() + + random.seed(self.seed) + + print(change_style("[Simulation #{}]".format(self.id), 'blue') + change_style(" Generating contents", "info")) + self.contents = Content.generate() + + print(change_style("[Simulation #{}]".format(self.id), 'blue') + change_style( + " Generating secondary users and fill up their caches", + "info")) + self.users = User.generate(self.env, self.contents) + + self.network = Network(self.env) + # Create PU arrivals + self.env.process(self.arrival_process(LAMBDA_PRIMARY_USER, UserType.PRIMARY)) + + # Create SU arrivals + self.env.process(self.arrival_process(LAMBDA_SECONDARY_USER, UserType.SECONDARY)) + + print(change_style("[Simulation #{}]".format(self.id), 'blue') + change_style(" Starting", "info")) + self.env.run(until=self.time) + print(change_style("[Simulation #{}]".format(self.id), 'blue') + change_style(" Ending", "info")) + + self.logger.save() + Simulation.semaphore.release() + + performance = self.calculate_performance() + Simulation.lock.acquire() + Simulation.performances['latency'] += performance['latency'] + Simulation.performances['p']['sq'] += performance['p']['sq'] + Simulation.performances['p']['hq']['base'] += performance['p']['hq']['base'] + Simulation.performances['p']['hq']['enh']['base_local_hit'] += performance['p']['hq']['enh']['base_local_hit'] + Simulation.performances['p']['hq']['enh']['base_d2d'] += performance['p']['hq']['enh']['base_d2d'] + Simulation.lock.release() diff --git a/cmpe49f/cache/models/user.py b/cmpe49f/cache/models/user.py new file mode 100644 index 0000000..5da669f --- /dev/null +++ b/cmpe49f/cache/models/user.py @@ -0,0 +1,207 @@ +from enum import Enum +from models.cache import Cache +from models.network import Network +from config import CHANNEL_BANDWIDTH, CACHE_CAPACITY, DEVICE_RADIUS, NOISE, LAMBDA_USERS_PPP, RADIUS, \ + NUMBER_OF_CACHE_TRY +from collections import Counter +import math +import random +import sys +from utils import * +import numpy as np +import config + + +class Point: + """ Represents cartesian location of the user """ + + def __init__(self, x, y): + self.x = x + self.y = y + + def distance(self, point): + """ Calculates distance between self and given point """ + return math.sqrt((self.x - point.x) ** 2 + (self.y - point.y) ** 2) + + +class UserType(Enum): + """ Represents type of user """ + PRIMARY = "PU" + SECONDARY = "SU" + + +class User: + """ Represents user in simulation """ + + def __init__(self, id, type, point, env, contents): + self.id = id + self.type = type + self.position = point + self.env = env + self.caches = [] + self.distances = [] + self.channel_id = None + self.serving = False + if type == UserType.SECONDARY: + self.fill_cache(contents) + # print([str(x) for x in sorted(self.caches, key=lambda k: k.id)]) + + def __str__(self): + return str(self.type.value) + '_' + str(self.id) + + def calculate_distances(self, users): + """ Calculates distance with other devices and sort them low to high """ + for user in users: + self.distances.append({"id": user.id, "distance": self.position.distance(user.position)}) + self.distances = sorted(self.distances, key=lambda k: k['distance']) + + def fill_cache(self, contents): + """ Fills cache storage of device with random content based on their popularity """ + cache = Cache.get_random(contents) + count = NUMBER_OF_CACHE_TRY + while self.get_remaining_size() > cache.size: + if not self.is_cached(cache): + self.caches.append(cache) + self.used_cache(cache) + count = NUMBER_OF_CACHE_TRY + cache = Cache.get_random(contents) + while self.get_remaining_size() < cache.size and count > 0: + cache = Cache.get_random(contents) + count -= 1 + + def get_channel_power(self, distance): + """ Calculates channel power that depends on distance""" + if self.type == UserType.PRIMARY: + return 4.7e13 / ( + (4 * math.pi * Network.get_channel_frequency(self.channel_id) * distance) ** 2) + elif self.type == UserType.SECONDARY: + return 2.6e13 / ( + (4 * math.pi * Network.get_channel_frequency(self.channel_id) * distance) ** 2) + + def get_channel_capacity(self, distance): + """ Calculates channel capacity with its channel power""" + return CHANNEL_BANDWIDTH * math.log2(1 + (self.get_channel_power(distance) / (CHANNEL_BANDWIDTH * NOISE))) + + def is_cached(self, cache): + """ Checks is the device has that content's cache """ + for cached in self.caches: + if cache.id == cached.id and cache.type == cached.type: + return True + return False + + def used_cache(self, cache): + """ Changes use frequency and access time of the used cache """ + for cached in self.caches: + if cache.id == cached.id and cache.type == cached.type: + cached.LFU += 1 + cached.LRU = self.env.now + + def get_service_time(self, cache, distance): + """ Calculates service time of given cache file """ + return cache.size / self.get_channel_capacity(distance) + + def get_used_size(self): + """ Returns used size of cache storage """ + return sum([cache.size for cache in self.caches]) + + def get_remaining_size(self): + """ Returns remaining size of cache storage """ + return CACHE_CAPACITY - self.get_used_size() + + def get_priority(self): + """ Returns priority of user for drop it """ + if self.type == UserType.PRIMARY: + return 1 + elif self.type == UserType.SECONDARY: + return 2 + + def look_other_users(self, cache, users): + for user_dic in self.distances: + if user_dic['id'] != self.id and users[user_dic['id']].is_cached(cache): + return users[user_dic['id']] + + def store_cache(self, cache, users, contents): + self.open_free_space(cache, users, contents) + self.caches.append(cache) + self.print("Store cache " + str(cache), 'blue') + + def open_free_space(self, new_cache, users, contents): + while new_cache.size > self.get_remaining_size(): + self.delete_cache(new_cache, users, contents) + + def delete_cache(self, new_cache, users, contents): + cache = None + if config.ALGORITHM == "LRU": + lr = {"index": 0, "time": self.env.now} + for i, cache in enumerate(self.caches): + if cache.LRU <= lr["time"]: + lr["index"] = i + lr["time"] = cache.LRU + cache = self.caches.pop(lr["index"]) + elif config.ALGORITHM == "LFU": + lf = {"index": 0, "frequency": sys.maxsize} + for i, cache in enumerate(self.caches): + if cache.LFU <= lf["frequency"]: + lf["index"] = i + lf["frequency"] = cache.LFU + cache = self.caches.pop(lf["index"]) + elif config.ALGORITHM == "RAND": + cache = self.caches.pop(random.choice(range(len(self.caches)))) + elif config.ALGORITHM == "MY": + cache_counts = Counter() + neighbors = [distance['id'] for distance in self.distances if + (distance['distance'] <= DEVICE_RADIUS and distance['id'] != self.id)] + for id in neighbors: + cache_count = [str(cache) for cache in users[id].caches] + cache_counts.update(cache_count) + + for cache in self.caches: + count = cache_counts.get(str(cache)) + cache.weight = (count if count is not None else 0) / contents[cache.id - 1].popularity + + cache_to_look = [cache for cache in self.caches if cache.type == new_cache.type] + if not cache_to_look: + cache_to_look = self.caches + + cache_to_look.sort(key=lambda x: x.weight, reverse=True) + cache = cache_to_look[0] + self.caches.remove(cache) + + self.print("Remove cache " + str(cache), 'red') + + def print(self, message, style=None): + """ Prints user action to console """ + if config.NUMBER_OF_SIMULATIONS == 1: + print( + "{}{} {}".format(change_style((" [" + "{:.10f}".format(self.env.now) + "]").ljust(20), 'time'), + change_style((" " + str(self) + " ").ljust(9), 'user'), + change_style(message, style))) + + @staticmethod + def get_random(users): + """ Return random secondary user """ + return random.choice(users) + + @staticmethod + def generate(env, contents): + """ Generates random secondary user with PPP """ + users = [] + n = np.random.poisson(LAMBDA_USERS_PPP * np.pi * RADIUS ** 2) + u_1 = np.random.uniform(0.0, 1.0, n) # generate n uniformly distributed points + radii = np.zeros(n) # the radial coordinate of the points + angle = np.zeros(n) # the angular coordinate of the points + for i in range(n): + radii[i] = RADIUS * (np.sqrt(u_1[i])) + u_2 = np.random.uniform(0.0, 1.0, n) # generate another n uniformly distributed points + for i in range(n): + angle[i] = 2 * np.pi * u_2[i] + for i in range(n): + x = radii[i] * np.cos(angle[i]) + y = radii[i] * np.sin(angle[i]) + user = User(i, UserType.SECONDARY, Point(x, y), env, contents) + users.append(user) + + for user in users: + user.calculate_distances(users) + + return users diff --git a/cmpe49f/cache/utils.py b/cmpe49f/cache/utils.py new file mode 100644 index 0000000..df7fb61 --- /dev/null +++ b/cmpe49f/cache/utils.py @@ -0,0 +1,16 @@ +def change_style(str, style): + if style == "green": + return "\033[92m{}\033[00m".format(str) + elif style == "blue": + return "\033[36m{}\033[00m".format(str) + elif style == "info": + return "\033[92m \033[01m{}\033[00m".format(str) + elif style == "user": + return "\033[97m\033[104m{}\033[00m".format(str) + elif style == "bold": + return "\033[01m{}\033[00m".format(str) + elif style == "time": + return "\033[01m\033[97m\033[101m{}\033[00m".format(str) + elif style == "red": + return "\033[31m{}\033[00m".format(str) + return str