diff --git a/Buffer.py b/Buffer.py index bdc8277..48adf1b 100644 --- a/Buffer.py +++ b/Buffer.py @@ -11,7 +11,7 @@ def __init__(self, size, initialOcc, localNode, remoteNode, server): self.server = server self.localNode = localNode self.latency_sum = 0.0 - self.last_latency = 0 + self.last_latency = 0.0 self.remoteNode = remoteNode for i in range(initialOcc): self.dataq.appendleft(BittideFrame(sender_timestamp=-1,sender_phys_time=-1, signals=[])) diff --git a/Controllers/FuzzyPController.py b/Controllers/FuzzyPController.py new file mode 100644 index 0000000..a6e41ef --- /dev/null +++ b/Controllers/FuzzyPController.py @@ -0,0 +1,126 @@ +# File: Controllers/FuzzyPController.py + +import numpy as np +from Controllers.Controller import Controller, ControlResult # Assuming this path is correct + +class FuzzyPController(Controller): + def __init__(self, name, node, setpoint=50.0, error_input_gain=0.2, control_output_gain=-1.5): + """ + Initializes the Fuzzy P Controller for percentage-based occupancy. + + Args: + name (str): Name of the controller. + node (object): The node object this controller is associated with. + setpoint (float): Target buffer occupancy as a percentage (e.g., 50.0 for 50%). + error_input_gain (float): Scales raw percentage error to fuzzy logic's UoD. + For raw error +/-50% to scaled +/-10, gain is 10/50 = 0.2. + control_output_gain (float): Scales fuzzy logic's output to final control action. + (e.g., 15-30 to match Kp of 0.15-0.3). + """ + super().__init__(name, node, "FuzzyP") + self.setpoint = float(setpoint) + self.error_input_gain = float(error_input_gain) + self.control_output_gain = float(control_output_gain) + self.last_c = 0.0 + + def _calculate_fuzzy_output(self, scaled_error): + """ + Computes control output from scaled_error using fuzzy logic. + Based on fuzzyPController.m with UoD -10 to 10 for scaled error. + INPUT: scaled_error - Error signal, scaled to roughly [-10, 10] + OUTPUT: u_out - Control action, typically in [-5, 5] before output gain + """ + e = scaled_error + + # Membership functions based on MATLAB UoD -10 to 10 + mu_NL = 0.0 + if e <= -10: # mu_NL = mf(e, -10, -10, -5) + mu_NL = 1.0 + elif -10 < e < -5: + mu_NL = (-5 - e) / (-5 - (-10)) + + mu_NS = 0.0 + if -10 < e < 0: # mu_NS = mf(e, -10, -5, 0) + if e <= -5: + mu_NS = (e - (-10)) / (-5 - (-10)) + else: + mu_NS = (0 - e) / (0 - (-5)) + + mu_ZE = 0.0 + if -5 < e < 5: # mu_ZE = mf(e, -5, 0, 5) + if e <= 0: + mu_ZE = (e - (-5)) / (0 - (-5)) + else: + mu_ZE = (5 - e) / (5 - 0) + + mu_PS = 0.0 + if 0 < e < 10: # mu_PS = mf(e, 0, 5, 10) + if e <= 5: + mu_PS = (e - 0) / (5 - 0) + else: + mu_PS = (10 - e) / (10 - 5) + + mu_PL = 0.0 + if e >= 10: # mu_PL = mf(e, 5, 10, 10) + mu_PL = 1.0 + elif 5 < e < 10: + mu_PL = (e - 5) / (10 - 5) + + # Control values (singletons) + u_NL = -5 + u_NS = -2.5 + u_ZE = 0.0 + u_PS = 2.5 + u_PL = 5 + + numerator = (mu_NL * u_NL + mu_NS * u_NS + mu_ZE * u_ZE + + mu_PS * u_PS + mu_PL * u_PL) # + denominator = mu_NL + mu_NS + mu_ZE + mu_PS + mu_PL # + + if denominator != 0: + u_out = numerator / denominator # + else: + if e >= 10: + u_out = u_PL + elif e <= -10: + u_out = u_NL + else: + u_out = 0.0 # Default if no rule fires + return u_out + + def step(self, buffers_dict) -> ControlResult: # Renamed for clarity + buffer_vals_percent = [] + # buffers_dict is expected to be the self.buffers dictionary from the Node object + for buffer_key in buffers_dict: + buffer_obj = buffers_dict[buffer_key] + if hasattr(buffer_obj, 'running') and buffer_obj.running: + # This MUST return percentage (0-100) for this controller config to be correct + buffer_vals_percent.append(buffer_obj.get_occupancy_as_percent()) + + control_action = 0.0 + if len(buffer_vals_percent) > 0: + current_occupancy_percent = np.mean(buffer_vals_percent) + + # Calculate raw error in percentage terms + # error > 0 means occupancy is below setpoint (needs positive action to increase freq/fill) + # error < 0 means occupancy is above setpoint (needs negative action to decrease freq/slow fill) + error_percent = self.setpoint - current_occupancy_percent + + # Scale error for fuzzy logic input (target: +/-50% raw error -> +/-10 scaled error) + scaled_error_for_fuzzy = error_percent * self.error_input_gain + + # Get base fuzzy action (typically in -5 to 5 range) + fuzzy_output_base = self._calculate_fuzzy_output(scaled_error_for_fuzzy) + + # Scale fuzzy output to final control action + control_action = fuzzy_output_base * self.control_output_gain + + # # Optional: Print for debugging (ensure Node.py passes time 't' if used) + # current_time = 0 # Placeholder if time 't' is not passed to this step method + # print(f"T={current_time:.3f} Node {self.name}: Occ%={current_occupancy_percent:.1f}, SetPt%={self.setpoint:.1f}, RawErr%={error_percent:.1f}, ScaledErr={scaled_error_for_fuzzy:.2f}, FuzzyBase={fuzzy_output_base:.2f}, FreqCorr={control_action:.2f}") + + self.last_c = control_action + return ControlResult(freq_correction=control_action, do_tick=True) + + def get_control(self): + return self.last_c \ No newline at end of file diff --git a/Controllers/FuzzyPIController.py b/Controllers/FuzzyPIController.py new file mode 100644 index 0000000..086a186 --- /dev/null +++ b/Controllers/FuzzyPIController.py @@ -0,0 +1,78 @@ +import numpy as np +from Controllers.Controller import Controller, ControlResult + +class FuzzyPIController(Controller): + def __init__(self, name, node, setpoint=50.0, + error_input_gain=0.2, derror_input_gain=2.5, + dcontrol_output_gain=-0.5): + super().__init__(name, node, "FuzzyPI") + self.setpoint = float(setpoint) + self.error_input_gain = float(error_input_gain) + self.derror_input_gain = float(derror_input_gain) + self.dcontrol_output_gain = float(dcontrol_output_gain) + self.last_error_percent = 0.0 + self.last_control_action = 0.0 + self.input_mfs = { + 'NL': {'points': [-np.inf, -10, -5]}, 'NS': {'points': [-10, -5, 0]}, + 'ZE': {'points': [-5, 0, 5]}, 'PS': {'points': [0, 5, 10]}, + 'PL': {'points': [5, 10, np.inf]} + } + self.output_singletons = { + 'NL': -5.0, 'NS': -2.5, 'ZE': 0.0, 'PS': 2.5, 'PL': 5.0 + } + self.rule_base = [ + ['NL', 'NL', 'NL', 'NS', 'ZE'], ['NL', 'NL', 'NS', 'ZE', 'PS'], + ['NL', 'NS', 'ZE', 'PS', 'PL'], ['NS', 'ZE', 'PS', 'PL', 'PL'], + ['ZE', 'PS', 'PL', 'PL', 'PL'] + ] + self.mf_names = ['NL', 'NS', 'ZE', 'PS', 'PL'] + + def _fuzzify(self, crisp_value: float) -> dict: + memberships = {} + for name, mf in self.input_mfs.items(): + p = mf['points'] + mu = 0.0 + if p[0] == -np.inf and crisp_value <= p[2]: + mu = 1.0 if crisp_value <= p[1] else (p[2] - crisp_value) / (p[2] - p[1]) + elif p[2] == np.inf and crisp_value >= p[0]: + mu = 1.0 if crisp_value >= p[1] else (crisp_value - p[0]) / (p[1] - p[0]) + elif p[0] <= crisp_value <= p[1]: + mu = (crisp_value - p[0]) / (p[1] - p[0]) + elif p[1] < crisp_value <= p[2]: + mu = (p[2] - crisp_value) / (p[2] - p[1]) + memberships[name] = mu + return memberships + + def _calculate_fuzzy_output(self, scaled_error: float, scaled_derror: float) -> float: + mu_e = self._fuzzify(scaled_error) + mu_de = self._fuzzify(scaled_derror) + numerator, denominator = 0.0, 0.0 + for e_idx, e_name in enumerate(self.mf_names): + for de_idx, de_name in enumerate(self.mf_names): + output_mf_name = self.rule_base[e_idx][de_idx] + output_singleton_val = self.output_singletons[output_mf_name] + firing_strength = min(mu_e[e_name], mu_de[de_name]) + if firing_strength > 0: + numerator += firing_strength * output_singleton_val + denominator += firing_strength + return numerator / denominator if denominator != 0 else 0.0 + + def step(self, buffers_dict: dict) -> ControlResult: + buffer_vals_percent = [b.get_occupancy_as_percent() for b in buffers_dict.values() if b.running] + if buffer_vals_percent: + current_occupancy_percent = np.mean(buffer_vals_percent) + error_percent = self.setpoint - current_occupancy_percent + derror_percent = error_percent - self.last_error_percent + scaled_e = error_percent * self.error_input_gain + scaled_de = derror_percent * self.derror_input_gain + delta_control_base = self._calculate_fuzzy_output(scaled_e, scaled_de) + delta_control = delta_control_base * self.dcontrol_output_gain + control_action = self.last_control_action + delta_control + self.last_error_percent = error_percent + self.last_control_action = control_action + else: + control_action = self.last_control_action + return ControlResult(freq_correction=control_action, do_tick=True) + + def get_control(self) -> float: + return self.last_control_action \ No newline at end of file diff --git a/DelayGenerator.py b/DelayGenerator.py index add7a84..1306e98 100644 --- a/DelayGenerator.py +++ b/DelayGenerator.py @@ -1,33 +1,114 @@ -from math import pi, sin -import matplotlib.pyplot as plt - +import math +import random # Added for random number generation +# Imports for the __main__ example part +# from matplotlib.pylab import plt # Original in image +import matplotlib.pyplot as plt # More standard from numpy import linspace import numpy as np class DelayGenerator: - def __init__(self, jitter_size, jitter_frequency, spike_size, spike_width, spike_period, delay_size, delay_start,delay_end): + def __init__(self, jitter_size, jitter_frequency, + spike_size, spike_width, spike_period, + min_base_delay, max_base_delay, # Changed from delay_size + delay_start, delay_end): # delay_start/end are for spike occurrence window self.jitter_size = jitter_size self.jitter_frequency = jitter_frequency self.spike_size = spike_size self.spike_width = spike_width self.spike_period = spike_period - self.delay_size = delay_size + + # Store min and max for the random base delay + self.min_base_delay = min_base_delay + self.max_base_delay = max_base_delay + if self.min_base_delay > self.max_base_delay: + raise ValueError("min_base_delay cannot be greater than max_base_delay") + + # These parameters define the time window during which spikes can occur self.delay_start = delay_start self.delay_end = delay_end + + def get_delay(self, time): + # Calculate jitter component + jitter = self.jitter_size * math.sin(2 * math.pi * self.jitter_frequency * time) + + # Calculate spike component + spike = 0 + # Spikes only occur if spike_size, spike_width, and spike_period are meaningful positive values + if self.spike_size > 0 and self.spike_width > 0 and self.spike_period > 0: + # Check if current time is within a spike pulse (repeats every spike_period) + is_in_spike_pulse = (time % self.spike_period) < self.spike_width + # Check if current time is within the allowed window for spikes (delay_start to delay_end) + is_in_spike_window = (time >= self.delay_start) and (time <= self.delay_end) # Inclusive start/end for window + + if is_in_spike_pulse and is_in_spike_window: + spike = self.spike_size + + # Generate random base delay component + # This is the core change: base delay is now a random value in the defined range + random_base_component = random.uniform(self.min_base_delay, self.max_base_delay) + + # Total delay is the sum of components + total_delay = jitter + spike + random_base_component + + # Ensure delay is not negative (e.g., if jitter is large and negative) + return max(0, total_delay) + +# This block is for testing DelayGenerator.py directly. +# It shows how to instantiate the class with the new parameters. +if __name__ == "__main__": + + # Example parameters: + # Original params for reference from image: + # jitter_size=0.01, jitter_frequency=0.1, + # spike_size=0.2, spike_width=0.01, spike_period=250, + # delay_size=0.2 (now replaced), delay_start=20, delay_end=70 + + # New instantiation with min_base_delay and max_base_delay. + # Let's assume the previous delay_size was 0.2, and we want it to vary randomly + # between 0.1 and 0.3. + min_delay_example = 0.0 + max_delay_example = 0.0 - def get_delay(self,time): - jitter = self.jitter_size * sin((2 * pi * self.jitter_frequency) * time) - spike = self.spike_size if ((time+self.spike_width) % self.spike_period) < self.spike_width else 0 - delay = self.delay_size if (time > self.delay_start) and (time < self.delay_end) else 0 - return jitter+spike+delay + # If you want no jitter or spikes for a purely random delay test, set their sizes to 0. + example_jitter_size = 0.01 + example_spike_size = 0.2 + # To disable jitter for the test plot: example_jitter_size = 0 + # To disable spikes for the test plot: example_spike_size = 0 + + + myDelay = DelayGenerator(jitter_size=example_jitter_size, jitter_frequency=0.1, + spike_size=example_spike_size, spike_width=0.01, spike_period=250, + min_base_delay=min_delay_example, max_base_delay=max_delay_example, + delay_start=20, delay_end=70) + + time_range = linspace(0, 100, num=1000) # 1000 points for a clearer plot -if __name__ == "__main__": - myDelay = DelayGenerator(jitter_size=0.01,jitter_frequency=0.1,spike_size=0.2,spike_width=0.01,spike_period=350,delay_size=0,delay_start=70) - time_range = linspace(0,600,num=1000000) - midpoint_freq = lambda t : 0.2 + myDelay.get_delay(t) - vfunc = np.vectorize(midpoint_freq) - yvals = vfunc(time_range) - plt.plot(time_range,yvals,linewidth=1) - plt.ylim((0,0.6)) - plt.xlim((0,600)) + # Get delay values for each point in time + # Using a list comprehension is straightforward for this. + yvals = [myDelay.get_delay(t) for t in time_range] + + # The original plotting script had an offset, let's plot the direct output first + # midpoint_freq = lambda t: 0.2 + myDelay.get_delay(t) # Original in image + # vfunc = np.vectorize(midpoint_freq) # vectorize works by calling the function for each element + # yvals = vfunc(time_range) + + plt.figure(figsize=(10, 6)) + plt.plot(time_range, yvals, linewidth=1, label=f'Random Delay ({min_delay_example}-{max_delay_example})') + + # Add lines for min and max base delay to visualize the random range clearly if jitter/spikes are off + if example_jitter_size == 0 and example_spike_size == 0: + plt.axhline(y=min_delay_example, color='r', linestyle='--', label=f'Min Base Delay ({min_delay_example})') + plt.axhline(y=max_delay_example, color='g', linestyle='--', label=f'Max Base Delay ({max_delay_example})') + + plt.xlabel("Time") + plt.ylabel("Generated Delay") + plt.title("DelayGenerator Output with Random Base Delay") + plt.legend() + plt.grid(True) + # Adjust ylim based on expected output range + # plt.ylim(min(yvals) - 0.1 * max(1, abs(min(yvals))), max(yvals) + 0.1 * max(1, abs(max(yvals)))) + # Or set a fixed reasonable ylim if you know the approximate range + expected_min_plot = min_delay_example - example_jitter_size + expected_max_plot = max_delay_example + example_jitter_size + example_spike_size + plt.ylim(max(0, expected_min_plot - 0.1), expected_max_plot + 0.1) plt.show() \ No newline at end of file diff --git a/Node.py b/Node.py index b132146..b788690 100644 --- a/Node.py +++ b/Node.py @@ -77,8 +77,9 @@ def step(self, steptime): if self.runtime_interchanger is not None: controlResult = self.runtime_interchanger.step(self) + #else: controlResult = self.controller.step(self.buffers, steptime) else: controlResult = self.controller.step(self.buffers) - self.freq += controlResult.freq_correction + self.freq = self.initialFreq + controlResult.freq_correction if controlResult.do_tick: #telemetry### diff --git a/ParseConfig.py b/ParseConfig.py index bc287c4..d8542f5 100644 --- a/ParseConfig.py +++ b/ParseConfig.py @@ -1,3 +1,5 @@ +# ParseConfig.py + import json from Controllers.TriggeredReframer import TriggeredReframer from Controllers.PIDControl import PIDController @@ -5,55 +7,85 @@ from Controllers.FFP import FFP from Node import Node from dataclasses import dataclass -from Interchangers import PIDFFP -from Interchangers import ReframingInterchanger +#from Interchangers import PIDFFP +#from Interchangers import ReframingInterchanger from Controllers.Lag import LagController +from DelayGenerator import DelayGenerator + + +# Import both Fuzzy P and Fuzzy PI controllers +from Controllers.FuzzyPController import FuzzyPController +from Controllers.FuzzyPIController import FuzzyPIController + + @dataclass class BufferSettings: - size : int - initialOcc : int - localNode : str - remoteNode : str + size: int + initialOcc: int + localNode: str + remoteNode: str @dataclass class LinkSettings: - sourceNode : str - destNode : str - destInitialOcc : int - destCapacity : int - delay : float + sourceNode: str + destNode: str + destInitialOcc: int + destCapacity: int + delay_model: DelayGenerator def form_controller_from_config(ctrl_opts, nodes, nj): controller_type = str(ctrl_opts["type"]).upper() + controller = None # Initialize controller to None + if controller_type == "PID": - controller = PIDController(nj["id"], nodes[nj["id"]], float(ctrl_opts["kp"]), float(ctrl_opts["ki"]), - int(ctrl_opts["ki_window"]), float(ctrl_opts["kd"]), - int(ctrl_opts["diff_step"]), float(ctrl_opts["offset"])) + controller = PIDController(nj["id"], nodes[nj["id"]], float(ctrl_opts["kp"]), float(ctrl_opts["ki"]), + int(ctrl_opts["ki_window"]), float(ctrl_opts["kd"]), + int(ctrl_opts["diff_step"]), float(ctrl_opts["offset"])) elif controller_type == "REFRAMER": controller = Reframer(nj["id"], nodes[nj["id"]], float(ctrl_opts["kp"]), - float(ctrl_opts["settle_time"]), float(ctrl_opts["settle_distance"]), float(ctrl_opts["wait_time"])) + float(ctrl_opts["settle_time"]), float(ctrl_opts["settle_distance"]), float(ctrl_opts["wait_time"])) elif controller_type == "INTERCHANGEREFRAMER": controller = TriggeredReframer(nj["id"], nodes[nj["id"]], float(ctrl_opts["kp"]), - float(ctrl_opts["settle_time"]), float(ctrl_opts["settle_distance"]), float(ctrl_opts["wait_time"])) + float(ctrl_opts["settle_time"]), float(ctrl_opts["settle_distance"]), float(ctrl_opts["wait_time"])) elif controller_type == "FFP": controller = FFP(nj["id"], nodes[nj["id"]]) elif controller_type == "LAG": - controller = LagController(nj["id"], nodes[nj["id"]], float(ctrl_opts["kp"]), float(ctrl_opts["ki"]), - float(ctrl_opts["kd"]),float(ctrl_opts["lag_kp"]),float(ctrl_opts["lag_td"]), - float(ctrl_opts["lead_kp"]),float(ctrl_opts["lead_td"])) - + controller = LagController(nj["id"], nodes[nj["id"]], float(ctrl_opts["kp"]), float(ctrl_opts["ki"]), + float(ctrl_opts["kd"]),float(ctrl_opts["lag_kp"]),float(ctrl_opts["lag_td"]), + float(ctrl_opts["lead_kp"]),float(ctrl_opts["lead_td"])) + elif controller_type == "FUZZYP": #Add Fuzzy P controller logic + + controller = FuzzyPController( + name=nj["id"], + node=nodes[nj["id"]], + setpoint=float(ctrl_opts.get("setpoint", 50.0)), + error_input_gain=float(ctrl_opts.get("error_input_gain", 0.2)), + control_output_gain=float(ctrl_opts.get("control_output_gain", -1.5)) + ) + + elif controller_type == "FUZZYPI": #Add Fuzzy PI controller logic + controller = FuzzyPIController( + name=nj["id"], + node=nodes[nj["id"]], + setpoint=float(ctrl_opts.get("setpoint", 50.0)), + error_input_gain=float(ctrl_opts.get("error_input_gain", 0.2)), + derror_input_gain=float(ctrl_opts.get("derror_input_gain", 0.8)), + dcontrol_output_gain=float(ctrl_opts.get("dcontrol_output_gain", -0.1)) + ) + + else: print("Unknown control scheme " + str(ctrl_opts["type"])) exit(0) + return controller - -def load_nodes_from_config(path, serv): +def load_nodes_from_config(path, serv): nodes = {} links = {} - + with open(path, 'r') as conf: config_json = json.load(conf) nodes_json = config_json["nodes"] @@ -61,38 +93,90 @@ def load_nodes_from_config(path, serv): for nj in nodes_json: buffer_configs = nj["buffers"] - all_buffs = [] #remote buffer : buff - for buffer in buffer_configs: + all_buffs = [] + for buffer_conf in buffer_configs: all_buffs.append( - BufferSettings(int(buffer["capacity"]), - int(buffer["initial_occ"]), - nj["id"], - buffer["dst_label"])) - - for link in links_json: - source_id = link["source_id"] - if source_id != nj["id"]: continue - links[source_id] = {} - for destination in link["destinations"]: - #find destination buffer info - for nd in nodes_json: - if nd["id"] != destination['dest_node_id']: continue - for dest_buffer in nd["buffers"]: - if dest_buffer["dst_label"] == source_id: - remote_starting_occ = dest_buffer["initial_occ"] - remote_max_occ = dest_buffer["capacity"] - break - else: + BufferSettings(int(buffer_conf["capacity"]), + int(buffer_conf["initial_occ"]), + nj["id"], + buffer_conf["dst_label"])) + + for link_group_info in links_json: + source_id_in_link_group = link_group_info["source_id"] + + if source_id_in_link_group == nj["id"]: + if nj["id"] not in links: + links[nj["id"]] = {} + + for destination_info in link_group_info["destinations"]: + dest_node_id = destination_info["dest_node_id"] + + remote_starting_occ_val = None + remote_max_occ_val = None + found_dest_buffer = False + for dest_node_candidate_info in nodes_json: + if dest_node_candidate_info["id"] == dest_node_id: + for dest_buffer_on_dest_node in dest_node_candidate_info["buffers"]: + if dest_buffer_on_dest_node["dst_label"] == nj["id"]: + remote_starting_occ_val = dest_buffer_on_dest_node["initial_occ"] + remote_max_occ_val = dest_buffer_on_dest_node["capacity"] + found_dest_buffer = True + break + if found_dest_buffer: + break + + if not found_dest_buffer: + print(f"ERROR: Configuration error. Could not find buffer information on destination node '{dest_node_id}' for link from '{nj['id']}'. Skipping link.") + continue + + current_delay_gen = None + if "delay_params" in destination_info: + params = destination_info["delay_params"] + try: + spike_period_val = float(params.get("spike_period", 1.0)) + if spike_period_val <= 0 and float(params.get("spike_size", 0.0)) > 0: + print(f"Warning: 'spike_period' must be > 0 if 'spike_size' > 0 for link {nj['id']}->{dest_node_id}. Defaulting to 1.0.") + spike_period_val = 1.0 + + current_delay_gen = DelayGenerator( + jitter_size=float(params.get("jitter_size", 0.0)), + jitter_frequency=float(params.get("jitter_frequency", 0.1)), + spike_size=float(params.get("spike_size", 0.0)), + spike_width=float(params.get("spike_width", 0.01)), + spike_period=spike_period_val, + min_base_delay=float(params["min_base_delay"]), + max_base_delay=float(params["max_base_delay"]), + delay_start=float(params.get("delay_start", 0.0)), + delay_end=float(params.get("delay_end", 1.0e9)) + ) + except KeyError as e: + print(f"ERROR: Missing mandatory key {e} in 'delay_params' for link {nj['id']}->{dest_node_id}. Skipping link.") + continue + except ValueError as e: + print(f"ERROR: Invalid numeric value in 'delay_params' for link {nj['id']}->{dest_node_id}: {e}. Skipping link.") continue - - links[source_id][destination["dest_node_id"]] = LinkSettings(source_id, destination["dest_node_id"], - int(remote_starting_occ), - int(remote_max_occ), - float(destination["delay"])) - nodes[nj["id"]] = Node(nj["id"], all_buffs, float(nj["frequency"]), server=serv, outgoing_links=links[nj["id"]]) - - #check if this is a controller config, or a runtime interchange config: - + + elif "delay" in destination_info: + fixed_delay = float(destination_info["delay"]) + current_delay_gen = DelayGenerator( + min_base_delay=fixed_delay, max_base_delay=fixed_delay + ) + else: + print(f"WARNING: No delay information for link {nj['id']}->{dest_node_id}. Defaulting to 0 delay.") + current_delay_gen = DelayGenerator(min_base_delay=0.0, max_base_delay=0.0) + + links[nj["id"]][dest_node_id] = LinkSettings( + sourceNode=nj["id"], + destNode=dest_node_id, + destInitialOcc=int(remote_starting_occ_val), + destCapacity=int(remote_max_occ_val), + delay_model=current_delay_gen + ) + + node_outgoing_links = links.get(nj["id"], {}) + nodes[nj["id"]] = Node(nj["id"], all_buffs, float(nj["frequency"]), + server=serv, outgoing_links=node_outgoing_links) + if "interchange" in nj: interchange_type = nj["interchange"] if (str(interchange_type).upper() == "PIDFFP"): @@ -108,10 +192,10 @@ def load_nodes_from_config(path, serv): controller = form_controller_from_config(controller_cfg, nodes, nj) controller_name = controller_cfg["name"] interchanger.register_controller(controller_name, controller) + elif "controller" in nj: ctrl_opts = nj["controller"] controller = form_controller_from_config(ctrl_opts, nodes, nj) nodes[nj["id"]].set_controller(controller) - - + return (nodes, links) \ No newline at end of file diff --git a/Plotter.py b/Plotter.py index 6080ee2..4ae5b57 100644 --- a/Plotter.py +++ b/Plotter.py @@ -1,5 +1,8 @@ from enum import Enum import matplotlib.pyplot as plt +# We need to import DelayGenerator to use its type hint for clarity +from DelayGenerator import DelayGenerator + class Plotter: class PlotType(Enum): FullSize = 1 @@ -17,64 +20,80 @@ def __init__(self, nodes, links, fastest_freq, slowest_freq): for node in nodes.values(): self.node_labels.append(node.name) for link in links[node.name]: - self.buffer_labels.append(links[node.name][link].destNode + "->" + node.name) + self.buffer_labels.append(node.name + "->" + links[node.name][link].destNode) + self.timesteps = [] self.node_frequencies = [] - self.control_outputs = [] self.buffer_occupancies = [] - self.logical_delay = [] self.buffer_latencies = [] + # NEW: List to store the delay generator's output + self.generated_delays = [] self.jitter = [] - def plot(self,t): + # MODIFIED: Added 'delay_generator' parameter + def plot(self, t: float, delay_generator: DelayGenerator): step_frequencies = [] step_occupancies = [] - step_delays = [] - step_jitter = [] step_latencies = [] + for node in self.nodes.values(): - step_jitter.append(node.last_jitter) - step_latencies.extend(node.get_latencies()) + step_latencies.extend(node.get_latencies()) step_frequencies.append(node.get_frequency()) step_occupancies.extend(node.get_occupancies_as_percent()) - step_delays.extend(node.get_logical_delays()) + self.timesteps.append(t) - self.jitter.append(step_jitter) self.node_frequencies.append(step_frequencies) self.buffer_occupancies.append(step_occupancies) self.buffer_latencies.append(step_latencies) - self.logical_delay.append(step_delays) + # NEW: Get and store the current delay from the generator + self.generated_delays.append(delay_generator.get_delay(t)) + def render(self): + if not self.timesteps: + print("No data to plot.") + return if self.mode == self.PlotType.FullSize: - plt.figure(figsize=(4, 2), dpi=160) - plt.subplot(2, 1, 1) - plt.title("Frequency") - plt.ylabel("Hz") - plt.plot(self.timesteps, self.node_frequencies, label=self.node_labels) - plt.ylim([self.slowest_freq/1.01,self.fastest_freq*1.01]) - plt.legend(loc='best') - plt.subplot(2, 1, 2) - plt.title("Buffer Occupancies") + # MODIFIED: Changed from 3 to 4 subplots + fig, axs = plt.subplots(4, 1, figsize=(12, 10), dpi=160, sharex=True) + fig.suptitle("System Analysis", fontsize=16) + + # --- Plot 1: Frequency --- + axs[0].set_title("Node Frequency") + axs[0].set_ylabel("Hz") + axs[0].plot(self.timesteps, self.node_frequencies) + axs[0].legend(self.node_labels, loc='best') + axs[0].grid(True, linestyle='--', alpha=0.6) + axs[0].set_ylim(bottom=170, top=220) + + # --- Plot 2: Buffer Occupancies --- + axs[1].set_title("Buffer Occupancies") + axs[1].set_ylabel("Percent Full (%)") + axs[1].plot(self.timesteps, self.buffer_occupancies, alpha=0.8) + axs[1].legend(self.buffer_labels, loc='best', fontsize=8, ncol=3) + axs[1].set_ylim([30, 70]) + axs[1].grid(True, linestyle='--', alpha=0.6) - tnrfont = {'fontname':'Times New Roman'} + # --- Plot 3: Measured Network Latency --- + axs[2].set_title("Measured Network Latency (End-to-End)") + axs[2].set_ylabel("Latency (s)") + axs[2].plot(self.timesteps, self.buffer_latencies, alpha=0.8) + axs[2].legend(self.buffer_labels, loc='best', fontsize=8, ncol=3) + axs[2].grid(True, linestyle='--', alpha=0.6) + axs[2].set_ylim(bottom=0) - plt.ylabel("Percent Occ. ") - plt.xlabel("firing count") - plt.plot(self.timesteps, self.buffer_occupancies, label=self.buffer_labels,alpha=0.7) - plt.ylim([0,100]) - plt.xticks(fontproperties='Times New Roman', size=10) - plt.yticks(fontproperties='Times New Roman', size=10) - plt.legend(fontsize=8,loc='lower right',ncol=2, frameon=False, borderpad=0,labelspacing=0) + # --- NEW PLOT 4: Delay Generator Output --- + axs[3].set_title("Delay Generator Output") + axs[3].set_ylabel("Injected Delay (s)") + axs[3].set_xlabel("Time (s)") + axs[3].plot(self.timesteps, self.generated_delays, color='red', label="Generated Delay") + axs[3].legend(loc='best') + axs[3].grid(True, linestyle='--', alpha=0.6) + axs[3].set_ylim(bottom=0) + + plt.tight_layout(rect=[0, 0, 1, 0.96]) - plt.subplots_adjust(left=0.1, - bottom=0.15, - right=0.9, - top=0.85, - wspace=0.4, - hspace=0.8) elif self.mode == self.PlotType.Compact: - #render freq and occ in separate windows pass - plt.show() + plt.show() \ No newline at end of file diff --git a/System.py b/System.py index 1f8417f..66f3c05 100644 --- a/System.py +++ b/System.py @@ -45,7 +45,7 @@ graph_step = 1 / (2.0 * fastest_node_freq) if end_t == -1: #infer a default duration from node frequencies - end_t = 40000 / fastest_node_freq + end_t = 40000/ fastest_node_freq waiting_messages = deque() @@ -59,7 +59,7 @@ bar = IncrementalBar('Running', fill='@', suffix='%(percent)d%%') #progress bar delayGenerator = DelayGenerator( - jitter_size=0.0,jitter_frequency=0,spike_size=0,spike_width=0.0,spike_period=1,delay_size=1,delay_start=500,delay_end=20000) #modelling various delay attacks + jitter_size=0.0,jitter_frequency=0,spike_size=0,spike_width=0.0,spike_period=0,delay_start=500,delay_end=20000, min_base_delay=0.2, max_base_delay=0.5) #modelling various delay attacks ################################################# main simulation loop while t <= end_t and not crash: @@ -90,17 +90,19 @@ for outgoing_link in links[node.name]: #move output messages to outgoing links link = links[node.name][outgoing_link] if out.messages != None: - waiting_messages.append(WaitingMessage(link.sourceNode, link.destNode, t + link.delay + delayGenerator.get_delay(t), out.messages)) + waiting_messages.append(WaitingMessage(link.sourceNode, link.destNode, t + link.delay_model.get_delay(t) + delayGenerator.get_delay(t), out.messages)) for buffer in node.buffers: #transmit a backpressure message on reverse link (FFP) for link in links[node.buffers[buffer].remoteNode]: if links[node.buffers[buffer].remoteNode][link].destNode == node.name: - backpressure_messages.append(BackPressureMessage(node.name,node.buffers[buffer].remoteNode, t + links[node.buffers[buffer].remoteNode][link].delay, node.phase)) + backpressure_messages.append(BackPressureMessage(node.name,node.buffers[buffer].remoteNode, t + links[node.buffers[buffer].remoteNode][link].delay_model.get_delay(t), node.phase)) break # graph at a lower resolution than the simulation # if next_graph <= t: - plotter.plot(t) + #plotter.plot(t) + # Pass the current time and the delay generator instance to the plotter + plotter.plot(t, delayGenerator) next_graph += graph_step #jump the simulation time to the next simulation event (machine tick or message delivery) @@ -130,4 +132,3 @@ print("Average point to point latency: " + str(responsetime_sum / len(nodes)) + " ticks per simulated second") plotter.render() - \ No newline at end of file diff --git a/configs/three_fuzzy.json b/configs/three_fuzzy.json new file mode 100644 index 0000000..4bd6ace --- /dev/null +++ b/configs/three_fuzzy.json @@ -0,0 +1,153 @@ +{ + "nodes": [ + { + "id": "n0", + "controller": { + "type": "FuzzyP", + "setpoint": 50.0, + "error_input_gain": 0.2, + "control_output_gain": -0.15 + }, + "buffers": [ + { + "dst_label": "n1", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n2", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 200.0, + "meta_x": 166, + "meta_y": 175 + }, + { + "id": "n1", + "controller": { + "type": "FuzzyP", + "setpoint": 50.0, + "error_input_gain": 0.2, + "control_output_gain": -0.15 + }, + "buffers": [ + { + "dst_label": "n0", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n2", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 175.0, + "meta_x": 288, + "meta_y": 387 + }, + { + "id": "n2", + "controller": { + "type": "FuzzyP", + "setpoint": 50.0, + "error_input_gain": 0.2, + "control_output_gain": -0.15 + }, + "buffers": [ + { + "dst_label": "n1", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n0", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 150.0, + "meta_x": 427, + "meta_y": 173 + } + ], + "links": [ + { + "source_id": "n0", + "destinations": [ + { + "source_buffer_id": 0, + "dest_node_id": "n1", + "dest_buffer_id": 0, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 0.5, + "max_base_delay": 0.5, "delay_start": 0, "delay_end": 1.0e9 + } + }, + { + "source_buffer_id": 1, + "dest_node_id": "n2", + "dest_buffer_id": 1, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + } + ] + }, + { + "source_id": "n1", + "destinations": [ + { + "source_buffer_id": 0, + "dest_node_id": "n0", + "dest_buffer_id": 0, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + }, + { + "source_buffer_id": 1, + "dest_node_id": "n2", + "dest_buffer_id": 0, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + } + ] + }, + { + "source_id": "n2", + "destinations": [ + { + "source_buffer_id": 0, + "dest_node_id": "n1", + "dest_buffer_id": 1, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + }, + { + "source_buffer_id": 1, + "dest_node_id": "n0", + "dest_buffer_id": 1, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + } + ] + } + ] + } \ No newline at end of file diff --git a/configs/three_fuzzy_pi.json b/configs/three_fuzzy_pi.json new file mode 100644 index 0000000..f87c134 --- /dev/null +++ b/configs/three_fuzzy_pi.json @@ -0,0 +1,156 @@ +{ + "nodes": [ + { + "id": "n0", + "controller": { + "type": "FuzzyPI", + "setpoint": 50.0, + "error_input_gain": 0.0002, + "derror_input_gain": 0.3, + "dcontrol_output_gain": -5 + }, + "buffers": [ + { + "dst_label": "n1", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n2", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 200.0, + "meta_x": 166, + "meta_y": 175 + }, + { + "id": "n1", + "controller": { + "type": "FuzzyPI", + "setpoint": 50.0, + "error_input_gain": 0.0002, + "derror_input_gain": 0.3, + "dcontrol_output_gain": -5 + }, + "buffers": [ + { + "dst_label": "n0", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n2", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 190.0, + "meta_x": 288, + "meta_y": 387 + }, + { + "id": "n2", + "controller": { + "type": "FuzzyPI", + "setpoint": 50.0, + "error_input_gain": 0.0002, + "derror_input_gain": 0.3, + "dcontrol_output_gain": -5 + }, + "buffers": [ + { + "dst_label": "n1", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n0", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 180.0, + "meta_x": 427, + "meta_y": 173 + } + ], + "links": [ + { + "source_id": "n0", + "destinations": [ + { + "source_buffer_id": 0, + "dest_node_id": "n1", + "dest_buffer_id": 0, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 0.5, + "max_base_delay": 0.5, "delay_start": 0, "delay_end": 1.0e9 + } + }, + { + "source_buffer_id": 1, + "dest_node_id": "n2", + "dest_buffer_id": 1, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + } + ] + }, + { + "source_id": "n1", + "destinations": [ + { + "source_buffer_id": 0, + "dest_node_id": "n0", + "dest_buffer_id": 0, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + }, + { + "source_buffer_id": 1, + "dest_node_id": "n2", + "dest_buffer_id": 0, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + } + ] + }, + { + "source_id": "n2", + "destinations": [ + { + "source_buffer_id": 0, + "dest_node_id": "n1", + "dest_buffer_id": 1, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + }, + { + "source_buffer_id": 1, + "dest_node_id": "n0", + "dest_buffer_id": 1, + "delay_params": { + "jitter_size": 0.0, "jitter_frequency": 0.1, "spike_size": 0.0, + "spike_width": 0.01, "spike_period": 1, "min_base_delay": 1.0, + "max_base_delay": 1.0, "delay_start": 0, "delay_end": 1.0e9 + } + } + ] + } + ] + } \ No newline at end of file