├── requirements.txt ├── README.md └── src ├── Part 1.py ├── Part 2.py └── Part 3.py /requirements.txt: -------------------------------------------------------------------------------- 1 | actdiag==2.0.0 2 | alabaster==0.7.12 3 | arrow==1.2.2 4 | asciidoc==10.1.4 5 | Babel==2.10.1 6 | binaryornot==0.4.4 7 | blockdiag==2.0.1 8 | certifi==2021.10.8 9 | chardet==4.0.0 10 | charset-normalizer==2.0.12 11 | click==8.0.4 12 | colorama==0.4.4 13 | cookiecutter==1.7.3 14 | coverage==6.2 15 | cycler==0.11.0 16 | docutils==0.17.1 17 | funcparserlib==0.3.6 18 | idna==3.3 19 | imagesize==1.3.0 20 | importlib-metadata==4.8.3 21 | Jinja2==3.0.3 22 | jinja2-time==0.2.0 23 | joblib==1.1.0 24 | kiwisolver==1.3.1 25 | Markdown==3.3.6 26 | markdown-blockdiag==0.8.0 27 | MarkupSafe==2.0.1 28 | matplotlib==3.3.4 29 | md-mermaid==0.1.1 30 | numpy==1.19.5 31 | nwdiag==2.0.0 32 | packaging==21.3 33 | Pillow==8.4.0 34 | poyo==0.5.0 35 | Pygments==2.12.0 36 | pyparsing==3.0.7 37 | python-dateutil==2.8.2 38 | python-slugify==6.1.1 39 | pytz==2022.1 40 | requests==2.27.1 41 | scipy==1.5.4 42 | seqdiag==2.0.0 43 | six==1.16.0 44 | snowballstemmer==2.2.0 45 | Sphinx==4.5.0 46 | sphinxcontrib-applehelp==1.0.2 47 | sphinxcontrib-blockdiag==2.0.0 48 | sphinxcontrib-devhelp==1.0.2 49 | sphinxcontrib-htmlhelp==2.0.0 50 | sphinxcontrib-jsmath==1.0.1 51 | sphinxcontrib-qthelp==1.0.3 52 | sphinxcontrib-serializinghtml==1.1.5 53 | text-unidecode==1.3 54 | threadpoolctl==3.1.0 55 | torch==1.4.0 56 | torchvision==0.5.0 57 | typing_extensions==4.1.1 58 | urllib3==1.26.8 59 | webcolors==1.11.1 60 | yellowbrick==1.3.post1 61 | zipp==3.6.0 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Management and Content Delivery for Smart Networks** 2 | ## **Algorithms and Modeling** 3 | This project simulates the operation of a renewable powered charging station for electric vehicles (EVs) in a goods delivery service scenario. The charging station serves a fleet of electric vans and can draw energy from the electric power grid as well as from a set of photovoltaic (PV) panels. 4 | 5 | ## **Project Structure** 6 | The project consists of four Python files, each related to a specific task within the project. Here's an overview of the files: 7 | 8 | - ***task1.py***: Simulates a simple case where the charging station has a small size (NBSS) and assumes a fixed average arrival rate for the EVs. It analyzes the average waiting delay or the missed service probability. 9 | 10 | - ***task2.py***: Simulates the system operation over a 24-hour period, considering variable arrival rates of EVs depending on the time of the day. It tests different minimum charge levels (Bth) for picking up batteries from the BSS and evaluates the impact on system performance. 11 | 12 | - ***task3.py***: Simulates the system operation on a summer day and a winter day. It considers the presence of PV panels and evaluates the system performance under different PV panel sizes (SPV). It also analyzes the effect of combining different PV panel sizes with the charging station size (NBSS). 13 | 14 | The simulation results are presented through various performance metrics, which can be visualized using graphs. The README file doesn't specify the exact performance metrics to be evaluated, so it's up to the user to define and plot relevant metrics based on their specific needs. 15 | 16 | The analysis of the system performance can be conducted by comparing different simulation settings and parameters, such as the average arrival rate, minimum charge level, PV panel size, and charging station size. The README file also suggests investigating the warm-up transient period and evaluating the confidence level of the estimated performance metrics. -------------------------------------------------------------------------------- /src/Part 1.py: -------------------------------------------------------------------------------- 1 | # Import libraries 2 | import random 3 | from queue import PriorityQueue 4 | import pandas as pd 5 | from scipy.stats import ttest_ind 6 | import scipy.stats 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import os 10 | import inspect 11 | import warnings 12 | 13 | warnings.filterwarnings("ignore", category=RuntimeWarning) 14 | 15 | # Set working directory: 16 | fn = inspect.getframeinfo(inspect.currentframe()).filename 17 | os.chdir(os.path.dirname(os.path.abspath(fn))) 18 | 19 | 20 | # Definning inputs and key characteristics of the system 21 | C = 40 # Total capacity of each battery - 40 kWh given 22 | MAX_iC = 10 # Max Initial Charge of arriving batteries - assumed 23 | R = 15 # Rate of Charging at the SCS - 10 kW to 20 kW given 24 | M_C = 40 # Minimum Charge needed for EV to be picked up - given 25 | N_BSS = 2 # Number of Charging points at the Station - assumed 26 | 27 | INTER_ARR = 1 # Inter Arrival Time of EVs - constant assumed 28 | W_MAX = 1 # Maximum wait time of EVs before missed service - assumed 29 | 30 | SIM_TIME = 200 # Number of Hours that will be sequentially simulated 31 | 32 | 33 | # Set and Reset Initial Simulation Parameters 34 | # Counts of EVs and Customers in different queues 35 | bat_cnt_charging = 0 36 | bat_cnt_in_charge_queue = 0 37 | bat_cnt_in_standby = 0 38 | EV_cnt_in_queue = 0 39 | 40 | # Details of EVs and Customers in different queues for tracking purpose 41 | charge_queue = [] 42 | charging_bats = [] 43 | standby_queue = [] 44 | EV_queue = [] 45 | 46 | # Function to reset the initial parameters for different runs of the simulation 47 | def refresh_initial_params(): 48 | global bat_cnt_charging 49 | global bat_cnt_in_charge_queue 50 | global bat_cnt_in_standby 51 | global EV_cnt_in_queue 52 | global charge_queue 53 | global charging_bats 54 | global standby_queue 55 | global EV_queue 56 | 57 | bat_cnt_charging = 0 58 | bat_cnt_in_charge_queue = 0 59 | bat_cnt_in_standby = 0 60 | EV_cnt_in_queue = 0 61 | 62 | charge_queue = [] 63 | charging_bats = [] 64 | standby_queue = [] 65 | EV_queue = [] 66 | 67 | 68 | # Classes to define the various objects that need to be tracked - EVs, Batteries and Measurements 69 | # EVs defined by their arrival time and expected renege time 70 | class Van: 71 | def __init__(self, EV_id, arrival_time): 72 | self.id = EV_id 73 | self.arrival_time = arrival_time 74 | self.renege_time = self.arrival_time + W_MAX * 1.0001 75 | 76 | 77 | # Batteries defined by their arrival time and initial charge 78 | class Battery: 79 | def __init__(self, bat_id, arrival_time): 80 | self.id = bat_id 81 | self.arrival_time = arrival_time 82 | self.initial_charge = round(random.uniform(0, MAX_iC), 2) 83 | 84 | 85 | # Measurement objects for system performance metrics and to maintain list of EVs and Batteries 86 | class Measure: 87 | def __init__(self): 88 | 89 | self.EVs = [] 90 | self.Batteries = [] 91 | 92 | self.serviced_EVs = [] 93 | 94 | self.EVarr = 0 95 | self.EV_serviced = 0 96 | self.EV_reneged = 0 97 | 98 | self.bats_recharged = 0 99 | 100 | self.EV_waiting_delay = [] 101 | self.EV_avg_waiting_delay = [] 102 | 103 | self.EV_missed_service_prob = [] 104 | 105 | # self.avg_energy_cost_per_EV = [] 106 | 107 | self.EV_queue_length = [] 108 | self.avg_EV_queue_length = [] 109 | 110 | self.charge_queue_length = [] 111 | self.avg_charge_queue_length = [] 112 | 113 | self.standby_queue_length = [] 114 | self.avg_standby_queue_length = [] 115 | 116 | 117 | # Define functions to handle events in the system 118 | # Function to handle arrival event - EVs arrive, if recharged batteries are available they swap and if there is no charge queue, the discharged battery is recharged 119 | def arrival(EV_id, time, FES, charge_queue, EV_queue, standby_queue): 120 | global bat_cnt_in_standby 121 | global bat_cnt_in_charge_queue 122 | global bat_cnt_charging 123 | global EV_cnt_in_queue 124 | global data 125 | 126 | EV = Van(EV_id, time) 127 | 128 | bat_id = "B" + EV_id[1:] 129 | bat = Battery(bat_id, time) 130 | 131 | # print(EV_id + ' arrived at ' + str(time) + ' with ' + bat_id) 132 | 133 | # Update relevant counts and parameters for tracking 134 | data.EVarr += 1 135 | EV_cnt_in_queue += 1 136 | 137 | data.EVs.append(EV) 138 | data.Batteries.append(bat) 139 | 140 | EV_queue.append(EV_id) 141 | 142 | # Update system performance measurement metrics 143 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 144 | data.avg_EV_queue_length.append( 145 | { 146 | "time": time, 147 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 148 | / len(data.EV_queue_length), 149 | } 150 | ) 151 | 152 | data.EV_missed_service_prob.append( 153 | {"time": time, "prob": round(data.EV_reneged / data.EVarr, 2)} 154 | ) 155 | 156 | # Schedule next arrival of EV 157 | inter_arrival = INTER_ARR 158 | FES.put((time + inter_arrival, "E" + str(int(EV_id[1:]) + 1), "EV Arrival")) 159 | 160 | # Schedule renege of the EV 161 | FES.put((EV.renege_time, EV.id, "Renege")) 162 | 163 | # If there are recharged batteries available when an EV arrives, the exchange happens 164 | if bat_cnt_in_standby > 0: 165 | 166 | # Update relevant counts and parameters for tracking 167 | delivered_bat = standby_queue.pop(0) 168 | # print(EV_id + ' exchanges ' + bat_id + ' for ' + delivered_bat.pop(0) + ' and leaves') 169 | bat_cnt_in_charge_queue += 1 170 | bat_cnt_in_standby -= 1 171 | EV_cnt_in_queue -= 1 172 | 173 | charge_queue.append(bat_id) 174 | EV_queue.remove(EV_id) 175 | 176 | # Update system performance measurement metrics 177 | 178 | data.serviced_EVs.append(EV.id) 179 | data.EV_serviced += 1 180 | 181 | data.EV_waiting_delay.append( 182 | {"time": time, "wait_delay": time - EV.arrival_time} 183 | ) 184 | data.EV_avg_waiting_delay.append( 185 | { 186 | "time": time, 187 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 188 | / len(data.EV_waiting_delay), 189 | } 190 | ) 191 | 192 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 193 | data.avg_charge_queue_length.append( 194 | { 195 | "time": time, 196 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 197 | / len(data.charge_queue_length), 198 | } 199 | ) 200 | 201 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 202 | data.avg_standby_queue_length.append( 203 | { 204 | "time": time, 205 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 206 | / len(data.standby_queue_length), 207 | } 208 | ) 209 | 210 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 211 | data.avg_EV_queue_length.append( 212 | { 213 | "time": time, 214 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 215 | / len(data.EV_queue_length), 216 | } 217 | ) 218 | 219 | # If the exchange happens and there is a charging point free, then charging of the discharged battery starts 220 | if bat_cnt_charging < N_BSS: 221 | 222 | # Determine recharge time 223 | initial_charge = bat.initial_charge 224 | charging_rate = round(R, 2) 225 | recharge_time = round((C - initial_charge) / charging_rate, 2) 226 | 227 | # Update paramters used to track the simulation 228 | charging_bats.append(bat_id) 229 | charge_queue.remove(bat_id) 230 | # print(bat_id + ' is charging at ' + str(round(time,2))) 231 | bat_cnt_in_charge_queue -= 1 232 | bat_cnt_charging += 1 233 | 234 | # Update performance metric 235 | data.charge_queue_length.append( 236 | {"time": time, "qlength": len(charge_queue)} 237 | ) 238 | data.avg_charge_queue_length.append( 239 | { 240 | "time": time, 241 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 242 | / len(data.charge_queue_length), 243 | } 244 | ) 245 | 246 | # Schedule completion of charging process 247 | FES.put((time + recharge_time, bat.id, "Stand By")) 248 | 249 | 250 | # Function to handle completion of charging of batteries 251 | def bat_charge_completion(time, bat_id, FES, charge_queue, standby_queue): 252 | global bat_cnt_charging 253 | global bat_cnt_in_charge_queue 254 | global bat_cnt_in_standby 255 | global data 256 | 257 | # Update relevant counts and parameters for tracking 258 | standby_queue.append(bat_id) 259 | charging_bats.remove(bat_id) 260 | # print(bat_id + ' charge completed at ' + str(round(time,2))) 261 | bat_cnt_charging -= 1 262 | bat_cnt_in_standby += 1 263 | 264 | # Update performance metrics 265 | data.bats_recharged += 1 266 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 267 | data.avg_standby_queue_length.append( 268 | { 269 | "time": time, 270 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 271 | / len(data.standby_queue_length), 272 | } 273 | ) 274 | 275 | # Inititate the charging process of next battery in queue 276 | if bat_cnt_in_charge_queue > 0: 277 | 278 | bat_id = charge_queue.pop(0) 279 | bat = [x for x in data.Batteries if x["id"] == bat_id][0] 280 | 281 | # Determine recharge time 282 | initial_charge = bat.initial_charge 283 | charging_rate = round(R, 2) 284 | recharge_time = round((C - initial_charge) / charging_rate, 2) 285 | 286 | # Update paramters used to track the simulation 287 | charging_bats.append(bat_id) 288 | # print(bat_id + ' is charging at ' + str(round(time,2))) 289 | bat_cnt_in_charge_queue -= 1 290 | bat_cnt_charging += 1 291 | 292 | # Update system performance measurement metrics 293 | 294 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 295 | data.avg_charge_queue_length.append( 296 | { 297 | "time": time, 298 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 299 | / len(data.charge_queue_length), 300 | } 301 | ) 302 | 303 | # Schedule completion of charging process 304 | FES.put((time + recharge_time, bat.id, "Stand By")) 305 | 306 | 307 | # Function to handle exchanging of discharged batteries for recharged batteries when thre are EVs in queue and a recharged battery becomes available 308 | def battery_exchange(time, bat_id, FES, EV_queue, standby_queue): 309 | global EV_cnt_in_queue 310 | global bat_cnt_in_standby 311 | global bat_cnt_charging 312 | global bat_cnt_in_charge_queue 313 | global data 314 | 315 | # If there are EVs in the queue and a recharged battery becomes available, then exchange event occurs. 316 | if len(EV_queue) > 0: 317 | 318 | bat_id = standby_queue.pop(0) 319 | 320 | EV_id = EV_queue.pop(0) 321 | # print(EV_id + ' takes ' + bat_id + ' and leaves at ' + str(round(time,2))) 322 | 323 | EV = [x for x in data.EVs if x.id == EV_id][0] 324 | 325 | # Update relevant tracking parameters 326 | EV_cnt_in_queue -= 1 327 | bat_cnt_in_standby -= 1 328 | 329 | bat_id = "B" + EV_id[1:] 330 | bat = [x for x in data.Batteries if x.id == bat_id][0] 331 | 332 | bat_cnt_in_charge_queue += 1 333 | charge_queue.append(bat_id) 334 | 335 | # Update system performance metrics 336 | 337 | data.serviced_EVs.append(EV_id) 338 | data.EV_serviced += 1 339 | 340 | data.EV_waiting_delay.append( 341 | {"time": time, "wait_delay": time - EV.arrival_time} 342 | ) 343 | data.EV_avg_waiting_delay.append( 344 | { 345 | "time": time, 346 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 347 | / len(data.EV_waiting_delay), 348 | } 349 | ) 350 | 351 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 352 | data.avg_EV_queue_length.append( 353 | { 354 | "time": time, 355 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 356 | / len(data.EV_queue_length), 357 | } 358 | ) 359 | 360 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 361 | data.avg_charge_queue_length.append( 362 | { 363 | "time": time, 364 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 365 | / len(data.charge_queue_length), 366 | } 367 | ) 368 | 369 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 370 | data.avg_standby_queue_length.append( 371 | { 372 | "time": time, 373 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 374 | / len(data.standby_queue_length), 375 | } 376 | ) 377 | 378 | # If there is a charging point available, then start charging the received discharged battery 379 | if bat_cnt_charging < N_BSS: 380 | 381 | # Determine recharge time 382 | initial_charge = bat.initial_charge 383 | charging_rate = round(R, 2) 384 | recharge_time = round((C - initial_charge) / charging_rate, 2) 385 | 386 | # Update paramters used to track the simulation 387 | charging_bats.append(bat_id) 388 | charge_queue.remove(bat_id) 389 | # print(bat_id + ' is charging at ' + str(round(time,2))) 390 | bat_cnt_in_charge_queue -= 1 391 | bat_cnt_charging += 1 392 | 393 | # Update system performance metrics 394 | data.charge_queue_length.append( 395 | {"time": time, "qlength": len(charge_queue)} 396 | ) 397 | data.avg_charge_queue_length.append( 398 | { 399 | "time": time, 400 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 401 | / len(data.charge_queue_length), 402 | } 403 | ) 404 | 405 | # Schedule completion of charging process 406 | FES.put((time + recharge_time, bat.id, "Stand By")) 407 | 408 | 409 | # Retrieve Electricity Price data 410 | 411 | 412 | def get_electricity_prices(filename="electricity_prices.csv"): 413 | 414 | prices = pd.read_csv(filename, header=None) 415 | 416 | spring_prices = prices.iloc[:, [1, 2, 3]] 417 | spring_prices.columns = ["Hour", "Season", "Price"] 418 | 419 | summer_prices = prices.iloc[:, [1, 4, 5]] 420 | summer_prices.columns = ["Hour", "Season", "Price"] 421 | 422 | fall_prices = prices.iloc[:, [1, 6, 7]] 423 | fall_prices.columns = ["Hour", "Season", "Price"] 424 | 425 | winter_prices = prices.iloc[:, [1, 8, 9]] 426 | winter_prices.columns = ["Hour", "Season", "Price"] 427 | 428 | electricity_prices = spring_prices.append( 429 | [summer_prices, fall_prices, winter_prices] 430 | ).reset_index(drop=True) 431 | electricity_prices["Season"] = electricity_prices["Season"].apply( 432 | lambda x: x.replace(":", "") 433 | ) 434 | 435 | return electricity_prices 436 | 437 | 438 | # Run a simulation from time = 0 to SIM_TIME 439 | 440 | 441 | def main_simulation(RANDOM_SEED): 442 | 443 | global bat_cnt_in_charge_queue 444 | global bat_cnt_in_standby 445 | global EV_cnt_in_queue 446 | global data 447 | 448 | # Set seed 449 | random.seed(RANDOM_SEED) 450 | 451 | # electricity_prices = get_electricity_prices() 452 | 453 | data = Measure() 454 | 455 | # Define event queue 456 | FES = PriorityQueue() 457 | 458 | # Initialize starting events 459 | time = 0 460 | FES.put((INTER_ARR, "E0", "EV Arrival")) 461 | 462 | bat_cnt_in_standby = N_BSS 463 | standby_queue = ["IB" + str(i) for i in range(bat_cnt_in_standby)] 464 | 465 | # Starting performance metrics 466 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 467 | data.avg_standby_queue_length.append( 468 | { 469 | "time": time, 470 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 471 | / len(data.standby_queue_length), 472 | } 473 | ) 474 | 475 | # Simulate until defined simulation time 476 | while time < SIM_TIME: 477 | 478 | # Get the immediate next scheduled event 479 | (time, client_id, event_type) = FES.get() 480 | 481 | # If the event is an EV renege event, 482 | if event_type == "Renege": 483 | 484 | if client_id not in data.serviced_EVs: 485 | 486 | # Update relevant simulation tracking parameters 487 | EV_queue.remove(client_id) 488 | # print(client_id + ' misses service due to long wait time and leaves.') 489 | EV_cnt_in_queue -= 1 490 | 491 | # Update system performance measurement metrics 492 | data.EV_reneged += 1 493 | data.EV_missed_service_prob.append( 494 | {"time": time, "prob": round(data.EV_reneged / data.EVarr, 2)} 495 | ) 496 | 497 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 498 | data.avg_EV_queue_length.append( 499 | { 500 | "time": time, 501 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 502 | / len(data.EV_queue_length), 503 | } 504 | ) 505 | 506 | next 507 | 508 | # For other events, call the corresponding event handler function 509 | elif event_type == "EV Arrival": 510 | arrival(client_id, time, FES, charge_queue, EV_queue, standby_queue) 511 | elif event_type == "Stand By": 512 | bat_charge_completion(time, client_id, FES, charge_queue, standby_queue) 513 | battery_exchange(time, client_id, FES, EV_queue, standby_queue) 514 | 515 | 516 | # Defining support functions to help analyze and visualize the simulation results 517 | # Plots a performance metric over time 518 | def plot_over_time(metric, title, ylabel): 519 | 520 | fig = plt.figure() 521 | ax = plt.axes() 522 | 523 | y = [t["avg_wait_delay"] for t in metric] 524 | x = [t["time"] for t in metric] 525 | 526 | ax.plot(x, y) 527 | plt.title(title) 528 | plt.xlabel("Time") 529 | plt.ylabel(ylabel) 530 | 531 | fig.show() 532 | 533 | 534 | # Defining functions to identify warm-up transient period in performance metrics 535 | 536 | # Fishman's method - graphical method based on column means 537 | def fishmans_method(avg_waiting_delay_list): 538 | 539 | metric = [] 540 | time = [] 541 | for i in range(len(avg_waiting_delay_list)): 542 | metric.append([x["avg_wait_delay"] for x in avg_waiting_delay_list[i]]) 543 | time.append([x["time"] for x in avg_waiting_delay_list[i]]) 544 | 545 | avg_metric = pd.DataFrame(metric).dropna(axis=1).mean().tolist() 546 | avg_time = pd.DataFrame(time).dropna(axis=1).mean().tolist() 547 | 548 | fig = plt.figure() 549 | ax = plt.axes() 550 | ax.plot(avg_time, avg_metric) 551 | plt.title("Smoothed based on Column Means - Fishman Approach") 552 | plt.xlabel("Time") 553 | plt.ylabel("Average Waiting Delay of EVs (in hours)") 554 | 555 | fig.show() 556 | 557 | return avg_metric, avg_time 558 | 559 | 560 | # Marginal Standard Error Rule (MSER) - assumes that observations in second half are closer to steady state mean 561 | def mser_method(smoothed_metric, smoothed_time): 562 | 563 | # Truncation point is selected such that it minimizes squared error between the selected points and the mean of the selected points 564 | mser = [] 565 | for d in range(int(len(smoothed_metric) - 1)): 566 | 567 | trunc_metric_list = smoothed_metric[(d + 1) :] 568 | avg_trunc_metric = sum(trunc_metric_list) / len(trunc_metric_list) 569 | 570 | mser.append( 571 | sum([(x - avg_trunc_metric) ** 2 for x in trunc_metric_list]) 572 | / (len(trunc_metric_list) ** 2) 573 | ) 574 | 575 | # fig = plt.figure() 576 | # ax = plt.axes() 577 | # ax.plot(mser[:-10]) 578 | # plt.title('MSER Approach for identifying truncation point') 579 | # plt.xlabel('Index') 580 | # plt.ylabel('MSER') 581 | # 582 | # fig.show() 583 | 584 | trunc_idx = mser[:-10].index(min(mser[:-10])) 585 | return smoothed_time[trunc_idx] 586 | 587 | 588 | # Randomization test - statistical method based on significant differences bewteen sequential batches of values 589 | def randomization_test(smoothed_metric, smoothed_time): 590 | 591 | N = int(len(smoothed_metric) / 20) 592 | avg_metric_batchmeans = [ 593 | sum(smoothed_metric[x : min(len(smoothed_metric), (x + N))]) 594 | / len(smoothed_metric[x : min(len(smoothed_metric), (x + N))]) 595 | for x in range(0, len(smoothed_metric), N) 596 | ] 597 | for i in range(1, len(avg_metric_batchmeans)): 598 | batch1 = avg_metric_batchmeans[:i] 599 | batch2 = avg_metric_batchmeans[i:] 600 | 601 | ttest, pval = ttest_ind(batch1, batch2) 602 | if pval > 0.05: 603 | break 604 | 605 | return smoothed_time[(i - 1) * N] 606 | 607 | 608 | # Functions to calculate confidence interval of a performance metric 609 | # Generic confidence interval function 610 | def mean_CI(data, confidence=0.95): 611 | a = 1.0 * np.array(data) 612 | n = len(a) 613 | m, se = np.mean(a), scipy.stats.sem(a) 614 | h = se * scipy.stats.t.ppf((1 + confidence) / 2.0, n - 1) 615 | return round(m, 4), round(m - h, 4), round(m + h, 4) 616 | 617 | 618 | # Truncating transient period and calculating confidence interval of the mean of the performance metric 619 | def confidence_interval(metric_list, transient_cutoff_time=50): 620 | 621 | metric = [] 622 | time = [] 623 | for i in range(len(metric_list)): 624 | metric.append([x["avg_wait_delay"] for x in metric_list[i]]) 625 | time.append([x["time"] for x in metric_list[i]]) 626 | 627 | metric_means = [] 628 | for i in range(len(metric)): 629 | cutoff_idx = next( 630 | x for x, val in enumerate(time[i]) if val > transient_cutoff_time 631 | ) 632 | metric_means.append(sum(metric[i][cutoff_idx:]) / len(metric[i][cutoff_idx:])) 633 | 634 | return mean_CI(metric_means) 635 | 636 | 637 | # Single Simulation Run 638 | refresh_initial_params() 639 | RANDOM_SEED = 42 640 | data = Measure() 641 | main_simulation(RANDOM_SEED) 642 | avg_waiting_delay = data.EV_avg_waiting_delay 643 | plot_over_time( 644 | metric=avg_waiting_delay, 645 | title="Average Waiting Delay of EVs from Simulation time = 0", 646 | ylabel="Avg. Waiting Delay", 647 | ) 648 | 649 | # Get point estimate of the performance metric 650 | transient_cutoff_time = 50 651 | cutoff_idx = next( 652 | x 653 | for x, val in enumerate([x["time"] for x in avg_waiting_delay]) 654 | if val > transient_cutoff_time 655 | ) 656 | point_estimate_mean = sum( 657 | [x["avg_wait_delay"] for x in avg_waiting_delay][cutoff_idx:] 658 | ) / len(avg_waiting_delay[cutoff_idx:]) 659 | 660 | 661 | # Multiple Independent Runs with different random seeds and different simulation runs 662 | runs = [100, 200, 300, 500, 750, 1000] 663 | 664 | print("\nConfidence Intervals of Average Wait Delay") 665 | print("Format: (Expected Value, Lower CI, Upper CI)\n") 666 | smoothed_metric_over_time = [] 667 | for N_runs in runs: 668 | 669 | # Multiple Independent Runs 670 | avg_waiting_delay_list = [] 671 | for i in range(N_runs): 672 | RANDOM_SEED = int(random.random() * 10000) 673 | 674 | refresh_initial_params() 675 | data = Measure() 676 | main_simulation(RANDOM_SEED) 677 | avg_waiting_delay_list.append(data.EV_avg_waiting_delay) 678 | 679 | smoothed_metric, smoothed_time = fishmans_method(avg_waiting_delay_list) 680 | mser_method(smoothed_metric, smoothed_time) 681 | randomization_test(smoothed_metric, smoothed_time) 682 | 683 | smoothed_metric_over_time.append([smoothed_metric, smoothed_time]) 684 | 685 | print( 686 | "Number of Simulation Runs = " 687 | + str(N_runs) 688 | + " and simulation time = " 689 | + str(SIM_TIME) 690 | ) 691 | print(confidence_interval(avg_waiting_delay_list)) 692 | print( 693 | "CI Width: " 694 | + str( 695 | confidence_interval(avg_waiting_delay_list)[2] 696 | - confidence_interval(avg_waiting_delay_list)[1] 697 | ) 698 | ) 699 | print("") 700 | -------------------------------------------------------------------------------- /src/Part 2.py: -------------------------------------------------------------------------------- 1 | # Import libraries 2 | import random 3 | from queue import PriorityQueue 4 | import pandas as pd 5 | from scipy.stats import ttest_ind 6 | import scipy.stats 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import os 10 | import inspect 11 | import warnings 12 | 13 | warnings.filterwarnings("ignore", category=RuntimeWarning) 14 | 15 | # Set working directory 16 | fn = inspect.getframeinfo(inspect.currentframe()).filename 17 | os.chdir(os.path.dirname(os.path.abspath(fn))) 18 | 19 | # Defining inputs and key characteristics of the system: 20 | 21 | 22 | C = 40 # Total capacity of each battery - 40 kWh given 23 | MAX_iC = 10 # Max Initial Charge of arriving batteries - assumed 24 | R = 15 # Rate of Charging at the SCS - 10 kW to 20 kW given 25 | B_TH = 20 # Minimum Charge needed for EV to be picked up during high demand periods 26 | N_BSS = 2 # Number of Charging points at the Station - assumed 27 | W_MAX = 1 # Maximum wait time of EVs before missed service - assumed 28 | 29 | 30 | # Assumptions of variable arrival rates of EVs depending on time of day 31 | 32 | EV_arr_rates = [0.25, 0.75, 1, 2] 33 | 34 | INTER_ARR = ( 35 | [EV_arr_rates[0]] * 7 36 | + [EV_arr_rates[1]] * 6 37 | + [EV_arr_rates[2]] * 7 38 | + [EV_arr_rates[3]] * 4 39 | ) 40 | 41 | partial_charge_times = [13, 14, 15, 16, 17, 18, 19, 20] 42 | 43 | 44 | # Set and Reset Initial Simulation Parameters: 45 | 46 | SIM_TIME = 500 # Number of Hours that will be sequentially simulated 47 | 48 | # Counts of EVs and Customers in different queues 49 | bat_cnt_charging = 0 50 | bat_cnt_in_charge_queue = 0 51 | bat_cnt_in_standby = 0 52 | EV_cnt_in_queue = 0 53 | 54 | # Details of EVs and Customers in different queues for tracking purpose 55 | charge_queue = [] 56 | charging_bats = [] 57 | standby_queue = [] 58 | EV_queue = [] 59 | 60 | # Function to reset the initial parameters for different runs of the simulation 61 | def refresh_initial_params(): 62 | global bat_cnt_charging 63 | global bat_cnt_in_charge_queue 64 | global bat_cnt_in_standby 65 | global EV_cnt_in_queue 66 | global charge_queue 67 | global charging_bats 68 | global standby_queue 69 | global EV_queue 70 | 71 | bat_cnt_charging = 0 72 | bat_cnt_in_charge_queue = 0 73 | bat_cnt_in_standby = 0 74 | EV_cnt_in_queue = 0 75 | 76 | charge_queue = [] 77 | charging_bats = [] 78 | standby_queue = [] 79 | EV_queue = [] 80 | 81 | 82 | # Classes to define the various objects that need to be tracked - EVs, Batteries and Measurements 83 | # EVs defined by their arrival time and expected renege time 84 | class Van: 85 | def __init__(self, EV_id, arrival_time): 86 | self.id = EV_id 87 | self.arrival_time = arrival_time 88 | self.renege_time = self.arrival_time + W_MAX * 1.0001 89 | 90 | 91 | # Batteries defined by their arrival time and initial charge 92 | class Battery: 93 | def __init__(self, bat_id, arrival_time): 94 | self.id = bat_id 95 | self.arrival_time = arrival_time 96 | self.initial_charge = round(random.uniform(0, MAX_iC), 2) 97 | 98 | 99 | # Measurement objects for system performance metrics and to maintain list of EVs and Batteries 100 | class Measure: 101 | def __init__(self): 102 | 103 | self.EVs = [] 104 | self.Batteries = [] 105 | 106 | self.serviced_EVs = [] 107 | 108 | self.EVarr = 0 109 | self.EV_serviced = 0 110 | self.EV_reneged = 0 111 | 112 | self.bats_recharged = 0 113 | 114 | self.EV_waiting_delay = [] 115 | self.EV_avg_waiting_delay = [] 116 | 117 | self.EV_missed_service_prob = [] 118 | 119 | # self.avg_energy_cost_per_EV = [] 120 | 121 | self.EV_queue_length = [] 122 | self.avg_EV_queue_length = [] 123 | 124 | self.charge_queue_length = [] 125 | self.avg_charge_queue_length = [] 126 | 127 | self.standby_queue_length = [] 128 | self.avg_standby_queue_length = [] 129 | 130 | 131 | # Defining functions to handle events in the system: 132 | # Function to handle arrival event - EVs arrive, if recharged batteries are available they swap and if there is no charge queue, the discharged battery is recharged 133 | def arrival(EV_id, time, FES, charge_queue, EV_queue, standby_queue): 134 | global bat_cnt_in_standby 135 | global bat_cnt_in_charge_queue 136 | global bat_cnt_charging 137 | global EV_cnt_in_queue 138 | global data 139 | 140 | EV = Van(EV_id, time) 141 | 142 | bat_id = "B" + EV_id[1:] 143 | bat = Battery(bat_id, time) 144 | 145 | # print(EV_id + ' arrived at ' + str(round(time,2)) + ' with ' + bat_id) 146 | 147 | # Update relevant counts and parameters for tracking: 148 | data.EVarr += 1 149 | EV_cnt_in_queue += 1 150 | 151 | data.EVs.append(EV) 152 | data.Batteries.append(bat) 153 | 154 | EV_queue.append(EV_id) 155 | 156 | # Update system performance measurement metrics: 157 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 158 | data.avg_EV_queue_length.append( 159 | { 160 | "time": time, 161 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 162 | / len(data.EV_queue_length), 163 | } 164 | ) 165 | 166 | data.EV_missed_service_prob.append( 167 | {"time": time, "prob": round(data.EV_reneged / data.EVarr, 2)} 168 | ) 169 | 170 | # Schedule next arrival of EV 171 | inter_arrival = random.expovariate(INTER_ARR[int(time % 24)]) 172 | FES.put((time + inter_arrival, "E" + str(int(EV_id[1:]) + 1), "EV Arrival")) 173 | 174 | # Schedule renege of the EV 175 | FES.put((EV.renege_time, EV.id, "Renege")) 176 | 177 | # If there are recharged batteries available when an EV arrives, the exchange happens: 178 | if bat_cnt_in_standby > 0: 179 | 180 | # Update relevant counts and parameters for tracking: 181 | delivered_bat = standby_queue.pop(0) 182 | # print(EV_id + ' exchanges ' + bat_id + ' for ' + delivered_bat + ' and leaves') 183 | bat_cnt_in_charge_queue += 1 184 | bat_cnt_in_standby -= 1 185 | EV_cnt_in_queue -= 1 186 | 187 | charge_queue.append(bat_id) 188 | EV_queue.remove(EV_id) 189 | 190 | # Update system performance measurement metrics: 191 | 192 | data.serviced_EVs.append(EV.id) 193 | data.EV_serviced += 1 194 | 195 | data.EV_waiting_delay.append( 196 | {"time": time, "wait_delay": time - EV.arrival_time} 197 | ) 198 | data.EV_avg_waiting_delay.append( 199 | { 200 | "time": time, 201 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 202 | / len(data.EV_waiting_delay), 203 | } 204 | ) 205 | 206 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 207 | data.avg_charge_queue_length.append( 208 | { 209 | "time": time, 210 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 211 | / len(data.charge_queue_length), 212 | } 213 | ) 214 | 215 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 216 | data.avg_standby_queue_length.append( 217 | { 218 | "time": time, 219 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 220 | / len(data.standby_queue_length), 221 | } 222 | ) 223 | 224 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 225 | data.avg_EV_queue_length.append( 226 | { 227 | "time": time, 228 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 229 | / len(data.EV_queue_length), 230 | } 231 | ) 232 | 233 | # If the exchange happens and there is a charging point free, then charging of the discharged battery starts 234 | if bat_cnt_charging < N_BSS: 235 | 236 | # Determine recharge time: 237 | initial_charge = bat.initial_charge 238 | charging_rate = round(R, 2) 239 | recharge_time = round((C - initial_charge) / charging_rate, 2) 240 | availability_alert_time = round((B_TH - initial_charge) / charging_rate, 2) 241 | 242 | # Update paramters used to track the simulation: 243 | charging_bats.append(bat_id) 244 | charge_queue.remove(bat_id) 245 | # print(bat_id + ' is charging at ' + str(round(time,2))) 246 | bat_cnt_in_charge_queue -= 1 247 | bat_cnt_charging += 1 248 | 249 | # Update performance metric: 250 | data.charge_queue_length.append( 251 | {"time": time, "qlength": len(charge_queue)} 252 | ) 253 | data.avg_charge_queue_length.append( 254 | { 255 | "time": time, 256 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 257 | / len(data.charge_queue_length), 258 | } 259 | ) 260 | 261 | # Schedule completion of charging process: 262 | FES.put((time + recharge_time, bat.id, "Stand By")) 263 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 264 | 265 | 266 | # Function to handle completion of charging of batteries 267 | def bat_charge_completion(time, bat_id, FES, charge_queue, standby_queue): 268 | global bat_cnt_charging 269 | global bat_cnt_in_charge_queue 270 | global bat_cnt_in_standby 271 | global data 272 | 273 | if bat_id in charging_bats: 274 | 275 | # Update relevant counts and parameters for tracking 276 | standby_queue.append(bat_id) 277 | charging_bats.remove(bat_id) 278 | # print(bat_id + ' charge completed at ' + str(round(time,2))) 279 | bat_cnt_charging -= 1 280 | bat_cnt_in_standby += 1 281 | 282 | # Update performance metrics 283 | data.bats_recharged += 1 284 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 285 | data.avg_standby_queue_length.append( 286 | { 287 | "time": time, 288 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 289 | / len(data.standby_queue_length), 290 | } 291 | ) 292 | 293 | # Inititate the charging process of next battery in queue 294 | if bat_cnt_in_charge_queue > 0: 295 | 296 | bat_id = charge_queue.pop(0) 297 | bat = [x for x in data.Batteries if x.id == bat_id][0] 298 | 299 | # Determine recharge time 300 | initial_charge = bat.initial_charge 301 | charging_rate = round(R, 2) 302 | recharge_time = round((C - initial_charge) / charging_rate, 2) 303 | availability_alert_time = round((B_TH - initial_charge) / charging_rate, 2) 304 | 305 | # Update paramters used to track the simulation 306 | charging_bats.append(bat_id) 307 | # print(bat_id + ' is charging at ' + str(round(time,2))) 308 | bat_cnt_in_charge_queue -= 1 309 | bat_cnt_charging += 1 310 | 311 | # Update system performance measurement metrics 312 | 313 | data.charge_queue_length.append( 314 | {"time": time, "qlength": len(charge_queue)} 315 | ) 316 | data.avg_charge_queue_length.append( 317 | { 318 | "time": time, 319 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 320 | / len(data.charge_queue_length), 321 | } 322 | ) 323 | 324 | # Schedule completion of charging process 325 | FES.put((time + recharge_time, bat.id, "Stand By")) 326 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 327 | 328 | 329 | # Function to handle exchanging of discharged batteries for recharged batteries when thre are EVs in queue and a recharged battery becomes available 330 | def battery_exchange(time, bat_id, FES, EV_queue, standby_queue): 331 | global EV_cnt_in_queue 332 | global bat_cnt_in_standby 333 | global bat_cnt_charging 334 | global bat_cnt_in_charge_queue 335 | global data 336 | 337 | # If there are EVs in the queue and a recharged battery becomes available, then exchange event occurs. 338 | if bat_id in standby_queue and len(EV_queue) > 0: 339 | 340 | bat_id = standby_queue.pop(0) 341 | 342 | EV_id = EV_queue.pop(0) 343 | # print(EV_id + ' takes ' + bat_id + ' and leaves at ' + str(round(time,2))) 344 | 345 | EV = [x for x in data.EVs if x.id == EV_id][0] 346 | 347 | # Update relevant tracking parameters 348 | EV_cnt_in_queue -= 1 349 | bat_cnt_in_standby -= 1 350 | 351 | bat_id = "B" + EV_id[1:] 352 | bat = [x for x in data.Batteries if x.id == bat_id][0] 353 | 354 | bat_cnt_in_charge_queue += 1 355 | charge_queue.append(bat_id) 356 | 357 | # Update system performance metrics 358 | 359 | data.serviced_EVs.append(EV_id) 360 | data.EV_serviced += 1 361 | 362 | data.EV_waiting_delay.append( 363 | {"time": time, "wait_delay": time - EV.arrival_time} 364 | ) 365 | data.EV_avg_waiting_delay.append( 366 | { 367 | "time": time, 368 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 369 | / len(data.EV_waiting_delay), 370 | } 371 | ) 372 | 373 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 374 | data.avg_EV_queue_length.append( 375 | { 376 | "time": time, 377 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 378 | / len(data.EV_queue_length), 379 | } 380 | ) 381 | 382 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 383 | data.avg_charge_queue_length.append( 384 | { 385 | "time": time, 386 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 387 | / len(data.charge_queue_length), 388 | } 389 | ) 390 | 391 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 392 | data.avg_standby_queue_length.append( 393 | { 394 | "time": time, 395 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 396 | / len(data.standby_queue_length), 397 | } 398 | ) 399 | 400 | # If there is a charging point available, then start charging the received discharged battery 401 | if bat_cnt_charging < N_BSS: 402 | 403 | # Determine recharge time 404 | initial_charge = bat.initial_charge 405 | charging_rate = round(R, 2) 406 | recharge_time = round((C - initial_charge) / charging_rate, 2) 407 | availability_alert_time = round((B_TH - initial_charge) / charging_rate, 2) 408 | 409 | # Update paramters used to track the simulation 410 | charging_bats.append(bat_id) 411 | charge_queue.remove(bat_id) 412 | # print(bat_id + ' is charging at ' + str(round(time,2))) 413 | bat_cnt_in_charge_queue -= 1 414 | bat_cnt_charging += 1 415 | 416 | # Update system performance metrics 417 | data.charge_queue_length.append( 418 | {"time": time, "qlength": len(charge_queue)} 419 | ) 420 | data.avg_charge_queue_length.append( 421 | { 422 | "time": time, 423 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 424 | / len(data.charge_queue_length), 425 | } 426 | ) 427 | 428 | # Schedule completion of charging process 429 | FES.put((time + recharge_time, bat.id, "Stand By")) 430 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 431 | 432 | 433 | def availability_alert(time, bat_id, FES, EV_queue, charge_queue): 434 | global EV_cnt_in_queue 435 | global bat_cnt_charging 436 | global bat_cnt_in_charge_queue 437 | global data 438 | 439 | # If there are EVs in the queue and a recharged battery becomes available, then exchange event occurs. 440 | if len(EV_queue) > 0: 441 | 442 | EV_id = EV_queue.pop(0) 443 | # print(EV_id + ' takes ' + bat_id + ' and leaves at ' + str(round(time,2)) + ' - partially charged') 444 | 445 | EV = [x for x in data.EVs if x.id == EV_id][0] 446 | 447 | # Update relevant tracking parameters 448 | EV_cnt_in_queue -= 1 449 | bat_cnt_charging -= 1 450 | 451 | charging_bats.remove(bat_id) 452 | 453 | data.serviced_EVs.append(EV_id) 454 | 455 | bat_id = "B" + EV_id[1:] 456 | bat = [x for x in data.Batteries if x.id == bat_id][0] 457 | 458 | bat_cnt_in_charge_queue += 1 459 | charge_queue.append(bat_id) 460 | 461 | # Initiate charging of the next battery in queue 462 | bat_id = charge_queue.pop(0) 463 | bat = [x for x in data.Batteries if x.id == bat_id][0] 464 | 465 | # Determine recharge time 466 | initial_charge = bat.initial_charge 467 | charging_rate = round(R, 2) 468 | recharge_time = round((C - initial_charge) / charging_rate, 2) 469 | availability_alert_time = round((B_TH - initial_charge) / charging_rate, 2) 470 | 471 | # Update paramters used to track the simulation 472 | charging_bats.append(bat_id) 473 | # print(bat_id + ' is charging at ' + str(round(time,2))) 474 | bat_cnt_in_charge_queue -= 1 475 | bat_cnt_charging += 1 476 | 477 | # Update system performance metrics 478 | 479 | data.serviced_EVs.append(EV_id) 480 | data.EV_serviced += 1 481 | 482 | data.EV_waiting_delay.append( 483 | {"time": time, "wait_delay": time - EV.arrival_time} 484 | ) 485 | data.EV_avg_waiting_delay.append( 486 | { 487 | "time": time, 488 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 489 | / len(data.EV_waiting_delay), 490 | } 491 | ) 492 | 493 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 494 | data.avg_EV_queue_length.append( 495 | { 496 | "time": time, 497 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 498 | / len(data.EV_queue_length), 499 | } 500 | ) 501 | 502 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 503 | data.avg_charge_queue_length.append( 504 | { 505 | "time": time, 506 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 507 | / len(data.charge_queue_length), 508 | } 509 | ) 510 | 511 | # Schedule completion of charging process 512 | FES.put((time + recharge_time, bat.id, "Stand By")) 513 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 514 | 515 | 516 | # Retrieve Electricity Price data 517 | 518 | 519 | def get_electricity_prices(filename="electricity_prices.csv"): 520 | 521 | prices = pd.read_csv(filename, header=None) 522 | 523 | spring_prices = prices.iloc[:, [1, 2, 3]] 524 | spring_prices.columns = ["Hour", "Season", "Price"] 525 | 526 | summer_prices = prices.iloc[:, [1, 4, 5]] 527 | summer_prices.columns = ["Hour", "Season", "Price"] 528 | 529 | fall_prices = prices.iloc[:, [1, 6, 7]] 530 | fall_prices.columns = ["Hour", "Season", "Price"] 531 | 532 | winter_prices = prices.iloc[:, [1, 8, 9]] 533 | winter_prices.columns = ["Hour", "Season", "Price"] 534 | 535 | electricity_prices = spring_prices.append( 536 | [summer_prices, fall_prices, winter_prices] 537 | ).reset_index(drop=True) 538 | electricity_prices["Season"] = electricity_prices["Season"].apply( 539 | lambda x: x.replace(":", "") 540 | ) 541 | 542 | return electricity_prices 543 | 544 | 545 | # Run a simulation from time = 0 to SIM_TIME 546 | 547 | 548 | def main_simulation(RANDOM_SEED): 549 | 550 | global bat_cnt_in_charge_queue 551 | global bat_cnt_in_standby 552 | global EV_cnt_in_queue 553 | global data 554 | 555 | # Set seed 556 | random.seed(RANDOM_SEED) 557 | 558 | # electricity_prices = get_electricity_prices() 559 | 560 | data = Measure() 561 | 562 | # Define event queue 563 | FES = PriorityQueue() 564 | 565 | # Initialize starting events 566 | time = 0 567 | FES.put((INTER_ARR[0], "E0", "EV Arrival")) 568 | 569 | bat_cnt_in_standby = N_BSS 570 | standby_queue = ["IB" + str(i) for i in range(bat_cnt_in_standby)] 571 | 572 | # Starting performance metrics 573 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 574 | data.avg_standby_queue_length.append( 575 | { 576 | "time": time, 577 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 578 | / len(data.standby_queue_length), 579 | } 580 | ) 581 | 582 | # Simulate until defined simulation time 583 | while time < SIM_TIME: 584 | 585 | # Get the immediate next scheduled event 586 | (time, client_id, event_type) = FES.get() 587 | 588 | # If the event is an EV renege event, 589 | if event_type == "Renege": 590 | 591 | if client_id not in data.serviced_EVs: 592 | 593 | # Update relevant simulation tracking parameters 594 | EV_queue.remove(client_id) 595 | # print(client_id + ' misses service due to long wait time and leaves.') 596 | EV_cnt_in_queue -= 1 597 | 598 | # Update system performance measurement metrics 599 | data.EV_reneged += 1 600 | data.EV_missed_service_prob.append( 601 | {"time": time, "prob": round(data.EV_reneged / data.EVarr, 2)} 602 | ) 603 | 604 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 605 | data.avg_EV_queue_length.append( 606 | { 607 | "time": time, 608 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 609 | / len(data.EV_queue_length), 610 | } 611 | ) 612 | 613 | next 614 | 615 | # For other events, call the corresponding event handler function 616 | elif event_type == "EV Arrival": 617 | arrival(client_id, time, FES, charge_queue, EV_queue, standby_queue) 618 | elif event_type == "Stand By": 619 | bat_charge_completion(time, client_id, FES, charge_queue, standby_queue) 620 | battery_exchange(time, client_id, FES, EV_queue, standby_queue) 621 | elif ( 622 | event_type == "Availability Alert" 623 | and int(time % 24) in partial_charge_times 624 | ): 625 | availability_alert(time, client_id, FES, EV_queue, charge_queue) 626 | 627 | 628 | # Defining support functions to help analyze and visualize the simulation results: 629 | 630 | # Plots a performance metric over time 631 | def plot_over_time(metric, title, ylabel): 632 | 633 | fig = plt.figure() 634 | ax = plt.axes() 635 | 636 | y = [t["avg_wait_delay"] for t in metric] 637 | x = [t["time"] for t in metric] 638 | 639 | ax.plot(x, y) 640 | plt.title(title) 641 | plt.xlabel("Time") 642 | plt.ylabel(ylabel) 643 | 644 | fig.show() 645 | 646 | 647 | # Defining support functions to help analyze and visualize the performance metrics 648 | # Fishman's method of column means to smoothen the trends 649 | 650 | 651 | def fishmans_method(avg_waiting_delay_list, colname, ylabel, transient_cutoff_time): 652 | 653 | metric = [] 654 | time = [] 655 | for i in range(len(avg_waiting_delay_list)): 656 | metric.append([x[colname] for x in avg_waiting_delay_list[i]]) 657 | time.append([x["time"] for x in avg_waiting_delay_list[i]]) 658 | 659 | avg_metric = pd.DataFrame(metric).dropna(axis=1).mean().tolist() 660 | avg_time = pd.DataFrame(time).dropna(axis=1).mean().tolist() 661 | 662 | cutoff_idx = [x for x, val in enumerate(avg_time) if val > transient_cutoff_time][0] 663 | avg_metric = avg_metric[cutoff_idx:] 664 | avg_time = [x for x in avg_time[cutoff_idx:]] 665 | 666 | fig = plt.figure() 667 | ax = plt.axes() 668 | ax.plot(avg_time, avg_metric) 669 | plt.title("Smoothed based on Column Means - Fishman Approach") 670 | plt.xlabel("Time") 671 | plt.ylabel(ylabel) 672 | 673 | fig.show() 674 | 675 | return avg_metric, avg_time 676 | 677 | 678 | # Generic confidence interval function 679 | def mean_CI(data, confidence=0.95): 680 | a = 1.0 * np.array(data) 681 | n = len(a) 682 | m, se = np.mean(a), scipy.stats.sem(a) 683 | h = se * scipy.stats.t.ppf((1 + confidence) / 2.0, n - 1) 684 | return round(m, 4), round(m - h, 4), round(m + h, 4) 685 | 686 | 687 | # Truncating transient period and calculating confidence interval of the mean of the performance metric 688 | def confidence_interval(avg_waiting_delay_list, colname, transient_cutoff_time): 689 | 690 | metric = [] 691 | time = [] 692 | for i in range(len(avg_waiting_delay_list)): 693 | metric.append([x[colname] for x in avg_waiting_delay_list[i]]) 694 | time.append([x["time"] for x in avg_waiting_delay_list[i]]) 695 | 696 | metric_means = [] 697 | for i in range(len(metric)): 698 | cutoff_idx = [ 699 | x for x, val in enumerate(time[i]) if val > transient_cutoff_time 700 | ][0] 701 | metric_means.append(sum(metric[i][cutoff_idx:]) / len(metric[i][cutoff_idx:])) 702 | 703 | return mean_CI(metric_means) 704 | 705 | 706 | # Analysis of Performance Metrics for the changed inputs 707 | # Define all performance metrics to be tracked 708 | EV_waiting_delay_list = [] 709 | EV_avg_waiting_delay_list = [] 710 | EV_missed_service_prob_list = [] 711 | EV_queue_length_list = [] 712 | avg_EV_queue_length_list = [] 713 | charge_queue_length_list = [] 714 | avg_charge_queue_length_list = [] 715 | standby_queue_length_list = [] 716 | avg_standby_queue_length_list = [] 717 | 718 | # Define number of runs and run the simulation 719 | N_runs = 200 720 | for i in range(N_runs): 721 | 722 | RANDOM_SEED = int(random.random() * 10000) 723 | 724 | refresh_initial_params() 725 | data = Measure() 726 | main_simulation(RANDOM_SEED) 727 | 728 | EV_waiting_delay_list.append(data.EV_waiting_delay) 729 | EV_avg_waiting_delay_list.append(data.EV_avg_waiting_delay) 730 | EV_missed_service_prob_list.append(data.EV_missed_service_prob) 731 | EV_queue_length_list.append(data.EV_queue_length) 732 | avg_EV_queue_length_list.append(data.avg_EV_queue_length) 733 | charge_queue_length_list.append(data.charge_queue_length) 734 | avg_charge_queue_length_list.append(data.avg_charge_queue_length) 735 | standby_queue_length_list.append(data.standby_queue_length) 736 | avg_standby_queue_length_list.append(data.avg_standby_queue_length) 737 | 738 | 739 | # Defining truncation time for the new inputs 740 | transient_cutoff_time = 0 741 | 742 | # Plot the performance metrics for visualization 743 | 744 | smoothed_metric, smoothed_time = fishmans_method( 745 | EV_waiting_delay_list, "wait_delay", "Wait Time", transient_cutoff_time 746 | ) 747 | smoothed_metric, smoothed_time = fishmans_method( 748 | EV_avg_waiting_delay_list, 749 | "avg_wait_delay", 750 | "Average Wait Time", 751 | transient_cutoff_time, 752 | ) 753 | smoothed_metric, smoothed_time = fishmans_method( 754 | EV_missed_service_prob_list, 755 | "prob", 756 | "Missed Service Probability - EV", 757 | transient_cutoff_time, 758 | ) 759 | smoothed_metric, smoothed_time = fishmans_method( 760 | EV_queue_length_list, "qlength", "EV Queue Length", transient_cutoff_time 761 | ) 762 | smoothed_metric, smoothed_time = fishmans_method( 763 | avg_EV_queue_length_list, 764 | "avg_qlength", 765 | "EV Average Queue Length", 766 | transient_cutoff_time, 767 | ) 768 | smoothed_metric, smoothed_time = fishmans_method( 769 | charge_queue_length_list, "qlength", "Charge Queue Length", transient_cutoff_time 770 | ) 771 | smoothed_metric, smoothed_time = fishmans_method( 772 | avg_charge_queue_length_list, 773 | "avg_qlength", 774 | "Average Charge Queue Length", 775 | transient_cutoff_time, 776 | ) 777 | smoothed_metric, smoothed_time = fishmans_method( 778 | standby_queue_length_list, "qlength", "Standy Queue Length", transient_cutoff_time 779 | ) 780 | smoothed_metric, smoothed_time = fishmans_method( 781 | avg_standby_queue_length_list, 782 | "avg_qlength", 783 | "Average Standy Queue Length", 784 | transient_cutoff_time, 785 | ) 786 | 787 | # Get the confidence interval of the performance metrics 788 | 789 | print( 790 | "Average Waiting Delay: " 791 | + str( 792 | confidence_interval( 793 | EV_avg_waiting_delay_list, "avg_wait_delay", transient_cutoff_time 794 | ) 795 | ) 796 | ) 797 | print( 798 | "EV Missed Service Probability: " 799 | + str( 800 | confidence_interval(EV_missed_service_prob_list, "prob", transient_cutoff_time) 801 | ) 802 | ) 803 | print( 804 | "Average EV Queue Length: " 805 | + str( 806 | confidence_interval( 807 | avg_EV_queue_length_list, "avg_qlength", transient_cutoff_time 808 | ) 809 | ) 810 | ) 811 | print( 812 | "Average Charge Queue Length: " 813 | + str( 814 | confidence_interval( 815 | avg_charge_queue_length_list, "avg_qlength", transient_cutoff_time 816 | ) 817 | ) 818 | ) 819 | print( 820 | "Average Standby Queue Length: " 821 | + str( 822 | confidence_interval( 823 | avg_standby_queue_length_list, "avg_qlength", transient_cutoff_time 824 | ) 825 | ) 826 | ) 827 | -------------------------------------------------------------------------------- /src/Part 3.py: -------------------------------------------------------------------------------- 1 | # Import libraries 2 | import random 3 | from queue import PriorityQueue 4 | import pandas as pd 5 | from scipy.stats import ttest_ind 6 | import scipy.stats 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import os 10 | import inspect 11 | import warnings 12 | 13 | warnings.filterwarnings("ignore", category=RuntimeWarning) 14 | 15 | # Set working directory: 16 | fn = inspect.getframeinfo(inspect.currentframe()).filename 17 | os.chdir(os.path.dirname(os.path.abspath(fn))) 18 | 19 | 20 | # Defining inputs and the system: 21 | C = 40 # Total capacity of each battery - 40 kWh given 22 | MAX_iC = 10 # Max Initial Charge of arriving batteries - assumed 23 | R = 15 # Rate of Charging at the SCS - 10 kW to 20 kW given 24 | B_TH = 20 # Minimum Charge needed for EV to be picked up during high demand periods 25 | N_BSS = 2 # Number of Charging points at the Station - assumed 26 | W_MAX = 1 # Maximum wait time of EVs before missed service - assumed 27 | window = [ 28 | 19, 29 | 20, 30 | 21, 31 | 22, 32 | 23, 33 | ] # Postponing of charging for a fraction of batteries during this period 34 | 35 | # Assumptions of variable arrival rates of EVs depending on time of day 36 | 37 | EV_arr_rates = [0.25, 0.75, 1, 2] 38 | 39 | INTER_ARR = ( 40 | [EV_arr_rates[0]] * 7 41 | + [EV_arr_rates[1]] * 6 42 | + [EV_arr_rates[2]] * 7 43 | + [EV_arr_rates[3]] * 4 44 | ) 45 | 46 | partial_charge_times = [13, 14, 15, 16, 17, 18, 19, 20] 47 | 48 | 49 | # Set and Reset Initial Simulation Parameters 50 | SIM_TIME = 500 # Number of Hours that will be sequentially simulated 51 | 52 | # Counts of EVs and Customers in different queues: 53 | bat_cnt_charging = 0 54 | bat_cnt_in_charge_queue = 0 55 | bat_cnt_in_standby = 0 56 | bat_cnt_postponed = 0 57 | EV_cnt_in_queue = 0 58 | 59 | # Details of EVs and Customers in different queues for tracking purpose: 60 | charge_queue = [] 61 | charging_bats = [] 62 | standby_queue = [] 63 | EV_queue = [] 64 | 65 | # Function to reset the initial parameters for different runs of the simulation: 66 | def refresh_initial_params(): 67 | global bat_cnt_charging 68 | global bat_cnt_in_charge_queue 69 | global bat_cnt_in_standby 70 | global bat_cnt_postponed 71 | global EV_cnt_in_queue 72 | global charge_queue 73 | global charging_bats 74 | global standby_queue 75 | global EV_queue 76 | 77 | bat_cnt_charging = 0 78 | bat_cnt_in_charge_queue = 0 79 | bat_cnt_in_standby = 0 80 | bat_cnt_postponed = 0 81 | EV_cnt_in_queue = 0 82 | 83 | charge_queue = [] 84 | charging_bats = [] 85 | standby_queue = [] 86 | EV_queue = [] 87 | 88 | 89 | # Classes to define the various objects that need to be tracked - EVs, Batteries and Measurements 90 | 91 | # EVs defined by their arrival time and expected renege time 92 | class Van: 93 | def __init__(self, EV_id, arrival_time): 94 | self.id = EV_id 95 | self.arrival_time = arrival_time 96 | self.renege_time = self.arrival_time + W_MAX * 1.0001 97 | 98 | 99 | # Batteries defined by their arrival time and initial charge 100 | class Battery: 101 | def __init__(self, bat_id, arrival_time): 102 | self.id = bat_id 103 | self.arrival_time = arrival_time 104 | self.initial_charge = round(random.uniform(0, MAX_iC), 2) 105 | self.recharge_start_time = 0 106 | self.recharge_end_time = 0 107 | 108 | 109 | # Measurement objects for system performance metrics and to maintain list of EVs and Batteries 110 | class Measure: 111 | def __init__(self): 112 | 113 | self.EVs = [] 114 | self.Batteries = [] 115 | 116 | self.serviced_EVs = [] 117 | 118 | self.EVarr = 0 119 | self.EV_serviced = 0 120 | self.EV_reneged = 0 121 | 122 | self.bats_recharged = 0 123 | 124 | self.EV_waiting_delay = [] 125 | self.EV_avg_waiting_delay = [] 126 | 127 | self.EV_missed_service_prob = [] 128 | 129 | self.recharge_start_times = {} 130 | self.recharge_end_times = {} 131 | 132 | self.util_per_hour = [0] * 24 133 | 134 | self.EV_queue_length = [] 135 | self.avg_EV_queue_length = [] 136 | 137 | self.charge_queue_length = [] 138 | self.avg_charge_queue_length = [] 139 | 140 | self.standby_queue_length = [] 141 | self.avg_standby_queue_length = [] 142 | 143 | 144 | # Defining functions to handle events in the system: 145 | 146 | # Function to handle arrival event - EVs arrive, if recharged batteries are available they swap and if there is no charge queue, the discharged battery is recharged 147 | def arrival(EV_id, time, FES, charge_queue, EV_queue, standby_queue): 148 | global bat_cnt_in_standby 149 | global bat_cnt_in_charge_queue 150 | global bat_cnt_charging 151 | global bat_cnt_postponed 152 | global EV_cnt_in_queue 153 | global data 154 | 155 | EV = Van(EV_id, time) 156 | 157 | bat_id = "B" + EV_id[1:] 158 | bat = Battery(bat_id, time) 159 | 160 | # print(EV_id + ' arrived at ' + str(round(time,2)) + ' with ' + bat_id) 161 | 162 | # Update relevant counts and parameters for tracking 163 | data.EVarr += 1 164 | EV_cnt_in_queue += 1 165 | 166 | data.EVs.append(EV) 167 | data.Batteries.append(bat) 168 | 169 | EV_queue.append(EV_id) 170 | 171 | # Update system performance measurement metrics 172 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 173 | data.avg_EV_queue_length.append( 174 | { 175 | "time": time, 176 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 177 | / len(data.EV_queue_length), 178 | } 179 | ) 180 | 181 | data.EV_missed_service_prob.append( 182 | {"time": time, "prob": round(data.EV_reneged / data.EVarr, 2)} 183 | ) 184 | 185 | # Schedule next arrival of EV 186 | inter_arrival = random.expovariate(INTER_ARR[int(time % 24)]) 187 | FES.put((time + inter_arrival, "E" + str(int(EV_id[1:]) + 1), "EV Arrival")) 188 | 189 | # Schedule renege of the EV 190 | FES.put((EV.renege_time, EV.id, "Renege")) 191 | 192 | # If there are recharged batteries available when an EV arrives, the exchange happens 193 | if bat_cnt_in_standby > 0: 194 | 195 | # Update relevant counts and parameters for tracking 196 | delivered_bat = standby_queue.pop(0) 197 | # print(EV_id + ' exchanges ' + bat_id + ' for ' + delivered_bat + ' and leaves') 198 | bat_cnt_in_charge_queue += 1 199 | bat_cnt_in_standby -= 1 200 | EV_cnt_in_queue -= 1 201 | 202 | charge_queue.append(bat_id) 203 | EV_queue.remove(EV_id) 204 | 205 | # Update system performance measurement metrics 206 | 207 | data.serviced_EVs.append(EV.id) 208 | data.EV_serviced += 1 209 | 210 | data.EV_waiting_delay.append( 211 | {"time": time, "wait_delay": time - EV.arrival_time} 212 | ) 213 | data.EV_avg_waiting_delay.append( 214 | { 215 | "time": time, 216 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 217 | / len(data.EV_waiting_delay), 218 | } 219 | ) 220 | 221 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 222 | data.avg_charge_queue_length.append( 223 | { 224 | "time": time, 225 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 226 | / len(data.charge_queue_length), 227 | } 228 | ) 229 | 230 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 231 | data.avg_standby_queue_length.append( 232 | { 233 | "time": time, 234 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 235 | / len(data.standby_queue_length), 236 | } 237 | ) 238 | 239 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 240 | data.avg_EV_queue_length.append( 241 | { 242 | "time": time, 243 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 244 | / len(data.EV_queue_length), 245 | } 246 | ) 247 | 248 | # If the exchange happens and there is a charging point free, then charging of the discharged battery starts 249 | if bat_cnt_charging < N_BSS: 250 | 251 | if random.choices([1, 0], [f, 1 - f])[0] == 1 and ( 252 | int(time) % 24 in window 253 | ): 254 | FES.put((time + random.uniform(0, T_max), bat.id, "Postpone")) 255 | # print('Charging of ' + bat.id + ' postponed') 256 | bat_cnt_in_charge_queue -= 1 257 | bat_cnt_postponed += 1 258 | 259 | charge_queue.remove(bat_id) 260 | 261 | else: 262 | # Determine recharge time 263 | initial_charge = bat.initial_charge 264 | charging_rate = round(R, 2) 265 | recharge_time = round((C - initial_charge) / charging_rate, 2) 266 | availability_alert_time = round( 267 | (B_TH - initial_charge) / charging_rate, 2 268 | ) 269 | 270 | # Update paramters used to track the simulation 271 | charging_bats.append(bat_id) 272 | charge_queue.remove(bat_id) 273 | # print(bat_id + ' is charging at ' + str(round(time,2))) 274 | bat_cnt_in_charge_queue -= 1 275 | bat_cnt_charging += 1 276 | 277 | # Update performance metric 278 | data.charge_queue_length.append( 279 | {"time": time, "qlength": len(charge_queue)} 280 | ) 281 | data.avg_charge_queue_length.append( 282 | { 283 | "time": time, 284 | "avg_qlength": sum( 285 | [x["qlength"] for x in data.charge_queue_length] 286 | ) 287 | / len(data.charge_queue_length), 288 | } 289 | ) 290 | 291 | data.recharge_start_times[bat.id] = time 292 | 293 | # Schedule completion of charging process 294 | FES.put((time + recharge_time, bat.id, "Stand By")) 295 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 296 | 297 | 298 | # Function to handle completion of charging of batteries 299 | def bat_charge_completion(time, bat_id, FES, charge_queue, standby_queue): 300 | global bat_cnt_charging 301 | global bat_cnt_in_charge_queue 302 | global bat_cnt_in_standby 303 | global bat_cnt_postponed 304 | global data 305 | 306 | if bat_id in charging_bats: 307 | 308 | # Update relevant counts and parameters for tracking 309 | standby_queue.append(bat_id) 310 | charging_bats.remove(bat_id) 311 | # print(bat_id + ' charge completed at ' + str(round(time,2))) 312 | bat_cnt_charging -= 1 313 | bat_cnt_in_standby += 1 314 | 315 | # Update performance metrics 316 | data.bats_recharged += 1 317 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 318 | data.avg_standby_queue_length.append( 319 | { 320 | "time": time, 321 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 322 | / len(data.standby_queue_length), 323 | } 324 | ) 325 | 326 | data.recharge_end_times[bat_id] = time 327 | 328 | # Inititate the charging process of next battery in queue 329 | if bat_cnt_in_charge_queue > 0: 330 | 331 | bat_id = charge_queue.pop(0) 332 | bat = [x for x in data.Batteries if x.id == bat_id][0] 333 | 334 | if random.choices([1, 0], [f, 1 - f])[0] == 1 and ( 335 | int(time) % 24 in window 336 | ): 337 | FES.put((time + random.uniform(0, T_max), bat_id, "Postpone")) 338 | # print('Charging of ' + bat_id + ' postponed') 339 | 340 | bat_cnt_in_charge_queue -= 1 341 | bat_cnt_postponed += 1 342 | 343 | else: 344 | 345 | # Determine recharge time 346 | initial_charge = bat.initial_charge 347 | charging_rate = round(R, 2) 348 | recharge_time = round((C - initial_charge) / charging_rate, 2) 349 | availability_alert_time = round( 350 | (B_TH - initial_charge) / charging_rate, 2 351 | ) 352 | 353 | # Update paramters used to track the simulation 354 | charging_bats.append(bat_id) 355 | # print(bat_id + ' is charging at ' + str(round(time,2))) 356 | bat_cnt_in_charge_queue -= 1 357 | bat_cnt_charging += 1 358 | 359 | # Update system performance measurement metrics 360 | 361 | data.charge_queue_length.append( 362 | {"time": time, "qlength": len(charge_queue)} 363 | ) 364 | data.avg_charge_queue_length.append( 365 | { 366 | "time": time, 367 | "avg_qlength": sum( 368 | [x["qlength"] for x in data.charge_queue_length] 369 | ) 370 | / len(data.charge_queue_length), 371 | } 372 | ) 373 | 374 | data.recharge_start_times[bat_id] = time 375 | 376 | # Schedule completion of charging process 377 | FES.put((time + recharge_time, bat.id, "Stand By")) 378 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 379 | 380 | 381 | # Function to handle exchanging of discharged batteries for recharged batteries when thre are EVs in queue and a recharged battery becomes available 382 | def battery_exchange(time, bat_id, FES, EV_queue, standby_queue): 383 | global EV_cnt_in_queue 384 | global bat_cnt_in_standby 385 | global bat_cnt_charging 386 | global bat_cnt_in_charge_queue 387 | global bat_cnt_postponed 388 | global data 389 | 390 | # If there are EVs in the queue and a recharged battery becomes available, then exchange event occurs. 391 | if bat_id in standby_queue and len(EV_queue) > 0: 392 | 393 | bat_id = standby_queue.pop(0) 394 | 395 | EV_id = EV_queue.pop(0) 396 | # print(EV_id + ' takes ' + bat_id + ' and leaves at ' + str(round(time,2))) 397 | 398 | EV = [x for x in data.EVs if x.id == EV_id][0] 399 | 400 | # Update relevant tracking parameters 401 | EV_cnt_in_queue -= 1 402 | bat_cnt_in_standby -= 1 403 | 404 | bat_id = "B" + EV_id[1:] 405 | bat = [x for x in data.Batteries if x.id == bat_id][0] 406 | 407 | bat_cnt_in_charge_queue += 1 408 | charge_queue.append(bat_id) 409 | 410 | # Update system performance metrics 411 | 412 | data.serviced_EVs.append(EV_id) 413 | data.EV_serviced += 1 414 | 415 | data.EV_waiting_delay.append( 416 | {"time": time, "wait_delay": time - EV.arrival_time} 417 | ) 418 | data.EV_avg_waiting_delay.append( 419 | { 420 | "time": time, 421 | "avg_wait_delay": sum([x["wait_delay"] for x in data.EV_waiting_delay]) 422 | / len(data.EV_waiting_delay), 423 | } 424 | ) 425 | 426 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 427 | data.avg_EV_queue_length.append( 428 | { 429 | "time": time, 430 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 431 | / len(data.EV_queue_length), 432 | } 433 | ) 434 | 435 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 436 | data.avg_charge_queue_length.append( 437 | { 438 | "time": time, 439 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 440 | / len(data.charge_queue_length), 441 | } 442 | ) 443 | 444 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 445 | data.avg_standby_queue_length.append( 446 | { 447 | "time": time, 448 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 449 | / len(data.standby_queue_length), 450 | } 451 | ) 452 | 453 | # If there is a charging point available, then start charging the received discharged battery 454 | if bat_cnt_charging < N_BSS: 455 | 456 | if random.choices([1, 0], [f, 1 - f])[0] == 1 and ( 457 | int(time) % 24 in window 458 | ): 459 | FES.put((time + random.uniform(0, T_max), bat_id, "Postpone")) 460 | # print('Charging of ' + bat_id + 'postponed') 461 | 462 | bat_cnt_in_charge_queue -= 1 463 | bat_cnt_postponed += 1 464 | 465 | charge_queue.remove(bat_id) 466 | 467 | else: 468 | 469 | # Determine recharge time 470 | initial_charge = bat.initial_charge 471 | charging_rate = round(R, 2) 472 | recharge_time = round((C - initial_charge) / charging_rate, 2) 473 | availability_alert_time = round( 474 | (B_TH - initial_charge) / charging_rate, 2 475 | ) 476 | 477 | # Update paramters used to track the simulation 478 | charging_bats.append(bat_id) 479 | charge_queue.remove(bat_id) 480 | # print(bat_id + ' is charging at ' + str(round(time,2))) 481 | bat_cnt_in_charge_queue -= 1 482 | bat_cnt_charging += 1 483 | 484 | # Update system performance metrics 485 | data.charge_queue_length.append( 486 | {"time": time, "qlength": len(charge_queue)} 487 | ) 488 | data.avg_charge_queue_length.append( 489 | { 490 | "time": time, 491 | "avg_qlength": sum( 492 | [x["qlength"] for x in data.charge_queue_length] 493 | ) 494 | / len(data.charge_queue_length), 495 | } 496 | ) 497 | 498 | data.recharge_start_times[bat_id] = time 499 | 500 | # Schedule completion of charging process 501 | FES.put((time + recharge_time, bat.id, "Stand By")) 502 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 503 | 504 | 505 | # Function to handle event of swapping partially recharged batteries 506 | def availability_alert(time, bat_id, FES, EV_queue, charge_queue): 507 | global EV_cnt_in_queue 508 | global bat_cnt_charging 509 | global bat_cnt_in_charge_queue 510 | global bat_cnt_postponed 511 | global data 512 | 513 | # If there are EVs in the queue and a recharged battery becomes available, then exchange event occurs. 514 | if len(EV_queue) > 0: 515 | 516 | EV_id = EV_queue.pop(0) 517 | # print(EV_id + ' takes ' + bat_id + ' and leaves at ' + str(round(time,2)) + ' - partially charged') 518 | 519 | EV = [x for x in data.EVs if x.id == EV_id][0] 520 | 521 | # Update performance metric 522 | data.recharge_end_times[bat_id] = time 523 | 524 | # Update relevant tracking parameters 525 | EV_cnt_in_queue -= 1 526 | bat_cnt_charging -= 1 527 | 528 | data.serviced_EVs.append(EV_id) 529 | 530 | charging_bats.remove(bat_id) 531 | 532 | bat_id = "B" + EV_id[1:] 533 | bat = [x for x in data.Batteries if x.id == bat_id][0] 534 | 535 | bat_cnt_in_charge_queue += 1 536 | charge_queue.append(bat_id) 537 | 538 | # Initiate charging of the next battery in queue 539 | bat_id = charge_queue.pop(0) 540 | bat = [x for x in data.Batteries if x.id == bat_id][0] 541 | 542 | if random.choices([1, 0], [f, 1 - f])[0] == 1 and (int(time) % 24 in window): 543 | FES.put((time + random.uniform(0, T_max), bat_id, "Postpone")) 544 | # print('Charging of ' + bat_id + 'postponed') 545 | 546 | bat_cnt_in_charge_queue -= 1 547 | bat_cnt_postponed += 1 548 | 549 | else: 550 | 551 | # Determine recharge time 552 | initial_charge = bat.initial_charge 553 | charging_rate = round(R, 2) 554 | recharge_time = round((C - initial_charge) / charging_rate, 2) 555 | availability_alert_time = round((B_TH - initial_charge) / charging_rate, 2) 556 | 557 | # Update paramters used to track the simulation 558 | charging_bats.append(bat_id) 559 | # print(bat_id + ' is charging at ' + str(round(time,2))) 560 | bat_cnt_in_charge_queue -= 1 561 | bat_cnt_charging += 1 562 | 563 | # Update system performance metrics 564 | 565 | data.serviced_EVs.append(EV_id) 566 | data.EV_serviced += 1 567 | 568 | data.EV_waiting_delay.append( 569 | {"time": time, "wait_delay": time - EV.arrival_time} 570 | ) 571 | data.EV_avg_waiting_delay.append( 572 | { 573 | "time": time, 574 | "avg_wait_delay": sum( 575 | [x["wait_delay"] for x in data.EV_waiting_delay] 576 | ) 577 | / len(data.EV_waiting_delay), 578 | } 579 | ) 580 | 581 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 582 | data.avg_EV_queue_length.append( 583 | { 584 | "time": time, 585 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 586 | / len(data.EV_queue_length), 587 | } 588 | ) 589 | 590 | data.charge_queue_length.append( 591 | {"time": time, "qlength": len(charge_queue)} 592 | ) 593 | data.avg_charge_queue_length.append( 594 | { 595 | "time": time, 596 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 597 | / len(data.charge_queue_length), 598 | } 599 | ) 600 | 601 | data.recharge_start_times[bat_id] = time 602 | 603 | # Schedule completion of charging process 604 | FES.put((time + recharge_time, bat.id, "Stand By")) 605 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 606 | 607 | 608 | # Function to handle battery recharge postponement event 609 | def postponement(time, bat_id, FES, charge_queue, charging_bats): 610 | global bat_cnt_in_charge_queue 611 | global bat_cnt_postponed 612 | global bat_cnt_charging 613 | global data 614 | 615 | bat_cnt_in_charge_queue += 1 616 | charge_queue.append(bat_id) 617 | bat_cnt_postponed -= 1 618 | 619 | if bat_cnt_charging < N_BSS: 620 | 621 | bat = [x for x in data.Batteries if x.id == bat_id][0] 622 | 623 | # Determine recharge time 624 | initial_charge = bat.initial_charge 625 | charging_rate = round(R, 2) 626 | recharge_time = round((C - initial_charge) / charging_rate, 2) 627 | availability_alert_time = round((B_TH - initial_charge) / charging_rate, 2) 628 | 629 | # Update paramters used to track the simulation 630 | charging_bats.append(bat_id) 631 | charge_queue.remove(bat_id) 632 | # print(bat_id + ' is charging at ' + str(round(time,2))) 633 | bat_cnt_in_charge_queue -= 1 634 | bat_cnt_charging += 1 635 | 636 | # Update performance metric 637 | data.charge_queue_length.append({"time": time, "qlength": len(charge_queue)}) 638 | data.avg_charge_queue_length.append( 639 | { 640 | "time": time, 641 | "avg_qlength": sum([x["qlength"] for x in data.charge_queue_length]) 642 | / len(data.charge_queue_length), 643 | } 644 | ) 645 | 646 | data.recharge_start_times[bat.id] = time 647 | 648 | # Schedule completion of charging process 649 | FES.put((time + recharge_time, bat.id, "Stand By")) 650 | FES.put((time + availability_alert_time, bat.id, "Availability Alert")) 651 | 652 | 653 | # Run a simulation from time = 0 to SIM_TIME 654 | 655 | 656 | def main_simulation(RANDOM_SEED): 657 | 658 | global bat_cnt_in_charge_queue 659 | global bat_cnt_in_standby 660 | global EV_cnt_in_queue 661 | global data 662 | 663 | # Set seed 664 | random.seed(RANDOM_SEED) 665 | 666 | data = Measure() 667 | 668 | # Define event queue 669 | FES = PriorityQueue() 670 | 671 | # Initialize starting events 672 | time = 0 673 | FES.put((random.expovariate(INTER_ARR[int(time % 24)]), "E0", "EV Arrival")) 674 | 675 | bat_cnt_in_standby = N_BSS 676 | standby_queue = ["IB" + str(i) for i in range(bat_cnt_in_standby)] 677 | 678 | # Starting performance metrics 679 | data.standby_queue_length.append({"time": time, "qlength": len(standby_queue)}) 680 | data.avg_standby_queue_length.append( 681 | { 682 | "time": time, 683 | "avg_qlength": sum([x["qlength"] for x in data.standby_queue_length]) 684 | / len(data.standby_queue_length), 685 | } 686 | ) 687 | 688 | # Simulate until defined simulation time 689 | while time < SIM_TIME: 690 | 691 | # Get the immediate next scheduled event 692 | (time, client_id, event_type) = FES.get() 693 | 694 | # If the event is an EV renege event, 695 | if event_type == "Renege": 696 | 697 | if client_id not in data.serviced_EVs: 698 | 699 | # Update relevant simulation tracking parameters 700 | EV_queue.remove(client_id) 701 | # print(client_id + ' misses service due to long wait time and leaves.') 702 | EV_cnt_in_queue -= 1 703 | 704 | # Update system performance measurement metrics 705 | data.EV_reneged += 1 706 | data.EV_missed_service_prob.append( 707 | {"time": time, "prob": round(data.EV_reneged / data.EVarr, 2)} 708 | ) 709 | 710 | data.EV_queue_length.append({"time": time, "qlength": len(EV_queue)}) 711 | data.avg_EV_queue_length.append( 712 | { 713 | "time": time, 714 | "avg_qlength": sum([x["qlength"] for x in data.EV_queue_length]) 715 | / len(data.EV_queue_length), 716 | } 717 | ) 718 | 719 | next 720 | 721 | # For other events, call the corresponding event handler function 722 | elif event_type == "EV Arrival": 723 | arrival(client_id, time, FES, charge_queue, EV_queue, standby_queue) 724 | elif event_type == "Stand By": 725 | bat_charge_completion(time, client_id, FES, charge_queue, standby_queue) 726 | battery_exchange(time, client_id, FES, EV_queue, standby_queue) 727 | elif ( 728 | event_type == "Availability Alert" 729 | and int(time % 24) in partial_charge_times 730 | ): 731 | availability_alert(time, client_id, FES, EV_queue, charge_queue) 732 | elif event_type == "Postpone": 733 | postponement(time, client_id, FES, charge_queue, charging_bats) 734 | 735 | 736 | # Retrieve Electricity Price data and PV Production data 737 | 738 | 739 | def get_electricity_prices(filename="electricity_prices.csv"): 740 | 741 | prices = pd.read_csv(filename, header=None) 742 | 743 | spring_prices = prices.iloc[:, [1, 2, 3]] 744 | spring_prices.columns = ["Hour", "Season", "Price"] 745 | 746 | summer_prices = prices.iloc[:, [1, 4, 5]] 747 | summer_prices.columns = ["Hour", "Season", "Price"] 748 | 749 | fall_prices = prices.iloc[:, [1, 6, 7]] 750 | fall_prices.columns = ["Hour", "Season", "Price"] 751 | 752 | winter_prices = prices.iloc[:, [1, 8, 9]] 753 | winter_prices.columns = ["Hour", "Season", "Price"] 754 | 755 | electricity_prices = spring_prices.append( 756 | [summer_prices, fall_prices, winter_prices] 757 | ).reset_index(drop=True) 758 | electricity_prices["Season"] = electricity_prices["Season"].apply( 759 | lambda x: x.replace(":", "") 760 | ) 761 | 762 | return electricity_prices 763 | 764 | 765 | def get_PV_capacity(filename="PVproduction_PanelSize1kWp.csv"): 766 | 767 | pv_production = pd.read_csv(filename) 768 | 769 | # Convert to seasons 770 | def m_to_s(m): 771 | if m in [3, 4, 5]: 772 | return "Spring" 773 | elif m in [6, 7, 8]: 774 | return "Summer" 775 | elif m in [9, 10, 11]: 776 | return "Fall" 777 | elif m in [12, 1, 2]: 778 | return "Winter" 779 | 780 | pv_production["Season"] = pv_production["Month"].apply(lambda x: m_to_s(x)) 781 | 782 | production_by_season = pv_production.groupby(["Season", "Hour"]).agg( 783 | {"Output power (W)": "mean"} 784 | ) 785 | production_by_season = production_by_season.reset_index() 786 | 787 | return production_by_season 788 | 789 | 790 | # Define support functions to help analyze and visualize the performance metrics: 791 | def util_bars(util_by_hour): # Plots utilization by hour 792 | 793 | avg_metric = pd.DataFrame(util_by_hour).mean().tolist() 794 | avg_time = [x for x in range(24)] 795 | 796 | fig = plt.figure() 797 | ax = plt.axes() 798 | ax.bar(avg_time, avg_metric) 799 | plt.title("Avg. BSS Utilization by Hour") 800 | plt.xlabel("Hour of day") 801 | plt.ylabel("Utilization %") 802 | 803 | fig.show() 804 | 805 | 806 | # Calculates electricity cost based on station hourly utilization 807 | def get_cost(util_by_hour, season, S_PV=100, N_BSS=2): 808 | 809 | avg_util = pd.DataFrame(util_by_hour).mean().tolist() 810 | 811 | avg_power_consumed = [x * R * N_BSS for x in avg_util] 812 | 813 | electricity_prices = get_electricity_prices() 814 | 815 | production_by_season = get_PV_capacity() 816 | production_summer = production_by_season.loc[ 817 | production_by_season["Season"] == "Summer", "Output power (W)" 818 | ].tolist() 819 | production_winter = production_by_season.loc[ 820 | production_by_season["Season"] == "Winter", "Output power (W)" 821 | ].tolist() 822 | 823 | production_summer = [x * S_PV / 1000 for x in production_summer] 824 | production_winter = [x * S_PV / 1000 for x in production_winter] 825 | 826 | if season == "Summer": 827 | power_from_grid = [ 828 | max(x - y, 0) for x, y in zip(avg_power_consumed, production_summer) 829 | ] 830 | prices_summer = electricity_prices.loc[ 831 | electricity_prices["Season"] == "SUMMER", "Price" 832 | ].tolist() 833 | cost = sum([x * y / 1000 for x, y in zip(power_from_grid, prices_summer)]) 834 | 835 | elif season == "Winter": 836 | power_from_grid = [ 837 | max(x - y, 0) for x, y in zip(avg_power_consumed, production_winter) 838 | ] 839 | prices_winter = electricity_prices.loc[ 840 | electricity_prices["Season"] == "WINTER", "Price" 841 | ].tolist() 842 | cost = sum([x * y / 1000 for x, y in zip(power_from_grid, prices_winter)]) 843 | 844 | return cost 845 | 846 | 847 | # Full cost analysis for different values of S_PV and N_BSS 848 | def cost_analysis(S_PV=100, N_BSS=2): 849 | 850 | global data 851 | 852 | # Define all performance metrics to be tracked 853 | util_by_hour = [] 854 | cost_summer = [] 855 | cost_winter = [] 856 | 857 | # Define number of runs and run the simulation 858 | N_runs = 200 859 | for i in range(N_runs): 860 | 861 | RANDOM_SEED = int(random.random() * 10000) 862 | 863 | refresh_initial_params() 864 | data = Measure() 865 | main_simulation(RANDOM_SEED) 866 | 867 | recharged_bats = list(data.recharge_start_times.keys()) 868 | temp_util_list = [0] * SIM_TIME 869 | for bat in recharged_bats: 870 | 871 | recharge_start_time = data.recharge_start_times[bat] 872 | if bat in list(data.recharge_end_times.keys()): 873 | recharge_time = ( 874 | data.recharge_end_times[bat] - data.recharge_start_times[bat] 875 | ) 876 | else: 877 | recharge_time = SIM_TIME - data.recharge_start_times[bat] 878 | 879 | temp_time = (recharge_start_time - int(recharge_start_time)) + recharge_time 880 | temp_time_list = [1 for x in range(int(temp_time))] + [ 881 | round(temp_time - int(temp_time), 2) 882 | ] 883 | temp_time_list[0] = round( 884 | temp_time_list[0] - (recharge_start_time - int(recharge_start_time)), 2 885 | ) 886 | for j in range( 887 | int(recharge_start_time), 888 | min(int(recharge_start_time) + len(temp_time_list), SIM_TIME), 889 | ): 890 | temp_util_list[j] = ( 891 | temp_util_list[j] + temp_time_list[j - int(recharge_start_time)] 892 | ) 893 | 894 | temp_util_list_24 = [] 895 | for j in range(24): 896 | temp_util_list_24.append( 897 | sum([temp_util_list[x] for x in range(j, len(temp_util_list), 24)]) 898 | / len([temp_util_list[x] for x in range(j, len(temp_util_list), 24)]) 899 | ) 900 | 901 | util_by_hour.append([x / N_BSS for x in temp_util_list_24]) 902 | 903 | cost_summer.append(get_cost(util_by_hour, "Summer", S_PV, N_BSS)) 904 | cost_winter.append(get_cost(util_by_hour, "Winter", S_PV, N_BSS)) 905 | 906 | cost_summer = sum(cost_summer) / len(cost_summer) 907 | cost_winter = sum(cost_winter) / len(cost_winter) 908 | 909 | return util_by_hour, cost_summer, cost_winter 910 | 911 | 912 | # First part of the task - Combinations of S_PV and N_BSS 913 | # S_PV_list = [100, 200, 500, 750, 1000] 914 | # N_BSS_list = [2, 3, 5, 7, 10] 915 | # 916 | # cost_summer_list = [] 917 | # cost_winter_list = [] 918 | # for pv in S_PV_list: 919 | # temp_list_summer = [] 920 | # temp_list_winter = [] 921 | # for N_BSS in N_BSS_list: 922 | # util_by_hour, cost_summer, cost_winter = cost_analysis(pv, N_BSS) 923 | # temp_list_summer.append(cost_summer) 924 | # temp_list_winter.append(cost_winter) 925 | # 926 | # cost_summer_list.append(temp_list_summer) 927 | # cost_winter_list.append(temp_list_winter) 928 | # 929 | ##util_by_hour, cost_summer, cost_winter = cost_analysis(100, 2) 930 | # 931 | # pd.DataFrame(cost_summer_list) 932 | # pd.DataFrame(cost_winter_list) 933 | 934 | 935 | # Seond part of the question - postponement strategy 936 | f_list = [0, 0.25, 0.5, 0.75] 937 | T_max_list = [4, 5, 6, 7, 8] 938 | 939 | cost_summer_list = [] 940 | cost_winter_list = [] 941 | serviced_EVs_cnt = [] 942 | EV_missed_service_prob = [] 943 | for f in f_list: 944 | temp_list_summer = [] 945 | temp_list_winter = [] 946 | temp_list_serviced_EVs = [] 947 | temp_list_missed_prob = [] 948 | for T_max in T_max_list: 949 | util_by_hour, cost_summer, cost_winter = cost_analysis() 950 | temp_list_summer.append(cost_summer) 951 | temp_list_winter.append(cost_winter) 952 | temp_list_serviced_EVs.append(len(data.serviced_EVs)) 953 | temp_list_missed_prob.append(data.EV_missed_service_prob[-1]["prob"]) 954 | 955 | print("f: " + str(f) + ", " + "T_max: " + str(T_max)) 956 | util_bars(util_by_hour) 957 | 958 | cost_summer_list.append(temp_list_summer) 959 | cost_winter_list.append(temp_list_winter) 960 | serviced_EVs_cnt.append(temp_list_serviced_EVs) 961 | EV_missed_service_prob.append(temp_list_missed_prob) 962 | 963 | pd.DataFrame(cost_summer_list) 964 | pd.DataFrame(cost_winter_list) 965 | pd.DataFrame(serviced_EVs_cnt) 966 | pd.DataFrame(EV_missed_service_prob) 967 | --------------------------------------------------------------------------------