From 5aa7683dc27d83c8c78ebf245fe2ffcfa77b505d Mon Sep 17 00:00:00 2001 From: Korol-Yin <92257496+Korol-Yin@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:13:26 +1200 Subject: [PATCH] Add Random Delay Generator. Fuzzy P Controller is added. The Results from this branch could be compared with RandomDelay_FuzzyController branch --- .vscode/launch.json | 47 +++++++ Controllers/FuzzyPController.py | 126 ++++++++++++++++++ DelayGenerator.py | 119 ++++++++++++++--- ParseConfig.py | 163 +++++++++++++++++------ System.py | 14 +- configs/three_fuzzy.json | 153 ++++++++++++++++++++++ configs/three_pid_randomdelay.json | 204 +++++++++++++++++++++++++++++ debug_config.py | 73 +++++++++++ debug_controller.py | 85 ++++++++++++ run_test.py | 97 ++++++++++++++ 10 files changed, 1016 insertions(+), 65 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 Controllers/FuzzyPController.py create mode 100644 configs/three_fuzzy.json create mode 100644 configs/three_pid_randomdelay.json create mode 100644 debug_config.py create mode 100644 debug_controller.py create mode 100644 run_test.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..932ca7c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,47 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug PID Controller", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/System.py", + "args": [ + "--conf", "configs/three_pid.json", + "--duration", "10.0" + ], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": {}, + "stopOnEntry": false, + "justMyCode": false + }, + { + "name": "Debug FuzzyP Controller", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/System.py", + "args": [ + "--conf", "configs/three_fuzzy.json", + "--duration", "10.0" + ], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": {}, + "stopOnEntry": false, + "justMyCode": false + }, + { + "name": "Debug Config Parsing", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/debug_config.py", + "args": ["configs/three_pid.json"], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": {}, + "stopOnEntry": true, + "justMyCode": false + } + ] +} 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/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/ParseConfig.py b/ParseConfig.py index bc287c4..d8c7d6e 100644 --- a/ParseConfig.py +++ b/ParseConfig.py @@ -3,11 +3,13 @@ from Controllers.PIDControl import PIDController from Controllers.Reframer import Reframer from Controllers.FFP import FFP -from Node import Node +from Node import Node # Assuming Node.py is in a location accessible by this import from dataclasses import dataclass from Interchangers import PIDFFP from Interchangers import ReframingInterchanger from Controllers.Lag import LagController +from Controllers.FuzzyPController import FuzzyPController +from DelayGenerator import DelayGenerator @dataclass class BufferSettings: @@ -22,26 +24,40 @@ class LinkSettings: destNode : str destInitialOcc : int destCapacity : int - delay : float + # MODIFICATION: Changed 'delay : float' to 'delay_model : DelayGenerator' + delay_model : DelayGenerator # This will now store an instance of DelayGenerator def form_controller_from_config(ctrl_opts, nodes, nj): + controller_type = str(ctrl_opts["type"]).upper() 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"])) + 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"])) + 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)) + ) + + else: print("Unknown control scheme " + str(ctrl_opts["type"])) @@ -50,51 +66,122 @@ def form_controller_from_config(ctrl_opts, nodes, nj): def load_nodes_from_config(path, serv): - nodes = {} - links = {} + # links is a dictionary where keys are source_node_id and + # values are dictionaries of {dest_node_id: LinkSettings_instance} + links = {} with open(path, 'r') as conf: config_json = json.load(conf) nodes_json = config_json["nodes"] links_json = config_json["links"] - for nj in nodes_json: + for nj in nodes_json: # Iterate over each node definition in the JSON buffer_configs = nj["buffers"] - all_buffs = [] #remote buffer : buff - for buffer in buffer_configs: + all_buffs = [] + for buffer_conf in buffer_configs: # Corrected variable name from 'buffer' to 'buffer_conf' all_buffs.append( - BufferSettings(int(buffer["capacity"]), - int(buffer["initial_occ"]), - nj["id"], - buffer["dst_label"])) + BufferSettings(int(buffer_conf["capacity"]), + int(buffer_conf["initial_occ"]), + nj["id"], + buffer_conf["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: + # This part processes links that originate from the current node 'nj' + for link_group_info in links_json: # Iterates over the main "links" array from JSON + source_id_in_link_group = link_group_info["source_id"] + + if source_id_in_link_group == nj["id"]: # We are processing links for the current node nj + if nj["id"] not in links: + links[nj["id"]] = {} # Initialize dict for this source node's links + + for destination_info in link_group_info["destinations"]: + dest_node_id = destination_info["dest_node_id"] + + # Find destination buffer info (remote_starting_occ, remote_max_occ) + # This part looks for the buffer on the destination node that is designated for traffic from nj["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"]: # dst_label points back to source + 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 + + # --- MODIFICATION START: Process delay information --- + current_delay_gen = None + if "delay_params" in destination_info: + params = destination_info["delay_params"] + try: + # Ensure spike_period is valid if spikes are enabled + 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"]), # Mandatory + max_base_delay=float(params["max_base_delay"]), # Mandatory + delay_start=float(params.get("delay_start", 0.0)), + delay_end=float(params.get("delay_end", 1.0e9)) # Default to a large number + ) + 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: # Handles float conversion errors + print(f"ERROR: Invalid numeric value in 'delay_params' for link {nj['id']}->{dest_node_id}: {e}. Skipping link.") continue + + elif "delay" in destination_info: # Fallback to old fixed delay format + fixed_delay = float(destination_info["delay"]) + current_delay_gen = DelayGenerator( + jitter_size=0.0, jitter_frequency=0.1, + spike_size=0.0, spike_width=0.01, spike_period=1.0, # spike_period must be > 0 + min_base_delay=fixed_delay, max_base_delay=fixed_delay, + delay_start=0.0, delay_end=1.0e9 + ) + else: + print(f"WARNING: No delay information ('delay_params' or 'delay') for link {nj['id']}->{dest_node_id}. Defaulting to 0 delay.") + current_delay_gen = DelayGenerator( + jitter_size=0.0, jitter_frequency=0.1, + spike_size=0.0, spike_width=0.01, spike_period=1.0, + min_base_delay=0.0, max_base_delay=0.0, + delay_start=0.0, delay_end=1.0e9 + ) - 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"]]) + 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 # Store the DelayGenerator instance + ) + - #check if this is a controller config, or a runtime interchange config: + # Create Node instance + # Ensure outgoing_links is a dict; if a node has no outgoing links, provide an empty dict. + 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) + # Controller and Interchanger setup (remains the same) if "interchange" in nj: interchange_type = nj["interchange"] + # ... (rest of interchanger logic) ... if (str(interchange_type).upper() == "PIDFFP"): interchanger = PIDFFP.PIDFFP("PIDFFP") elif (str(interchange_type).upper() == "ROBUST"): @@ -108,10 +195,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/System.py b/System.py index 1f8417f..9bdddce 100644 --- a/System.py +++ b/System.py @@ -26,7 +26,7 @@ serv = ControlServer(50000) else: serv = None - + nodes, links = load_nodes_from_config(args.conf, serv) t = 0.0 @@ -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 = 100000 / 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,12 +90,12 @@ 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 # @@ -128,6 +128,4 @@ print(node.buffers[buffer].latency_sum / node.phase) print("Average system throughput: " + str(throughput_sum / len(nodes)) + " ticks per simulated second") print("Average point to point latency: " + str(responsetime_sum / len(nodes)) + " ticks per simulated second") - plotter.render() - - \ No newline at end of file + 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_pid_randomdelay.json b/configs/three_pid_randomdelay.json new file mode 100644 index 0000000..0b66a8c --- /dev/null +++ b/configs/three_pid_randomdelay.json @@ -0,0 +1,204 @@ +{ + "nodes": [ + { + "id": "n0", + "controller": { + "type": "PID", + "kp": 0.15, + "ki": 0.00003, + "ki_window": 0, + "kd": 0.0, + "diff_step": 1, + "offset": 0.0 + }, + "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": "PID", + "kp": 0.15, + "ki": 0.00003, + "ki_window": 0, + "kd": 0.0, + "diff_step": 1, + "offset": 0.0 + }, + "buffers": [ + { + "dst_label": "n0", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n2", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 185.0, + "meta_x": 288, + "meta_y": 387 + }, + { + "id": "n2", + "controller": { + "type": "PID", + "kp": 0.15, + "ki": 0.00003, + "ki_window": 0, + "kd": 0.0, + "diff_step": 1, + "offset": 0.0 + }, + "buffers": [ + { + "dst_label": "n1", + "capacity": 1000, + "initial_occ": 500 + }, + { + "dst_label": "n0", + "capacity": 1000, + "initial_occ": 500 + } + ], + "frequency": 170.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.4, + "max_base_delay": 0.6, + "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": 0.8, + "max_base_delay": 1.2, + "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": 0.8, + "max_base_delay": 1.2, + "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": 0.8, + "max_base_delay": 1.2, + "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": 0.8, + "max_base_delay": 1.2, + "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": 0.8, + "max_base_delay": 1.2, + "delay_start": 0, + "delay_end": 1.0e9 + } + } + ] + } + ] + } \ No newline at end of file diff --git a/debug_config.py b/debug_config.py new file mode 100644 index 0000000..0b49f79 --- /dev/null +++ b/debug_config.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Debug helper script for bittide simulation +""" +import sys +import json +import traceback +from ParseConfig import load_nodes_from_config + +def debug_config_parsing(config_path): + """Debug configuration parsing step by step""" + print(f"šŸ” Debugging configuration parsing for: {config_path}") + print("=" * 50) + + try: + # Load and display config + with open(config_path, 'r') as f: + config = json.load(f) + + print("āœ… Config loaded successfully") + print(f"šŸ“‹ Found {len(config.get('nodes', []))} nodes") + print(f"šŸ“‹ Found {len(config.get('links', []))} link groups") + + # Try to parse nodes and links + print("\nšŸ”§ Parsing nodes and links...") + nodes, links = load_nodes_from_config(config_path, None) + + print(f"āœ… Successfully parsed {len(nodes)} nodes") + print(f"āœ… Successfully parsed {len(links)} link groups") + + # Display node information + print("\nšŸ“Š Node Details:") + for node_id, node in nodes.items(): + print(f" • {node_id}: freq={node.freq}Hz, buffers={len(node.buffers)}") + if hasattr(node, 'controller') and node.controller: + controller_type = type(node.controller).__name__ + print(f" Controller: {controller_type}") + + # Display buffer information + for buffer_name, buffer in node.buffers.items(): + print(f" Buffer {buffer_name}: capacity={buffer.capacity}, initial={buffer.occupancy}") + + # Display link information + print("\nšŸ”— Link Details:") + for source_id, destinations in links.items(): + print(f" From {source_id}:") + for dest_id, link_settings in destinations.items(): + print(f" → {dest_id}: delay_model={type(link_settings.delay_model).__name__}") + + return nodes, links + + except Exception as e: + print(f"āŒ Error during config parsing:") + print(f" {type(e).__name__}: {e}") + print("\nšŸ“ Traceback:") + traceback.print_exc() + return None, None + +def main(): + if len(sys.argv) < 2: + print("Usage: python debug_config.py ") + print("Available configs:") + import os + config_files = [f for f in os.listdir('configs') if f.endswith('.json')] + for cf in config_files: + print(f" - configs/{cf}") + return + + config_path = sys.argv[1] + debug_config_parsing(config_path) + +if __name__ == "__main__": + main() diff --git a/debug_controller.py b/debug_controller.py new file mode 100644 index 0000000..e66111a --- /dev/null +++ b/debug_controller.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Debug helper for controller behavior +""" +import sys +import json +import numpy as np +from ParseConfig import load_nodes_from_config + +def debug_controller_step(config_path, steps=10): + """Debug controller behavior step by step""" + print(f"šŸŽ® Debugging controller behavior for: {config_path}") + print("=" * 50) + + try: + # Load configuration + nodes, links = load_nodes_from_config(config_path, None) + + if not nodes: + print("āŒ Failed to load nodes") + return + + # Find nodes with controllers + controlled_nodes = [] + for node_id, node in nodes.items(): + if hasattr(node, 'controller') and node.controller: + controlled_nodes.append((node_id, node)) + controller_type = type(node.controller).__name__ + print(f"šŸ“Š Found controller: {node_id} -> {controller_type}") + + if not controlled_nodes: + print("āŒ No controllers found in configuration") + return + + print(f"\nšŸ”„ Running {steps} controller steps...") + + for step in range(steps): + print(f"\n--- Step {step + 1} ---") + + for node_id, node in controlled_nodes: + try: + # Get current buffer states + buffer_states = {} + for buffer_name, buffer in node.buffers.items(): + buffer_states[buffer_name] = buffer + print(f" Buffer {buffer_name}: {buffer.get_occupancy_as_percent():.1f}%") + + # Run controller step + if hasattr(node.controller, 'step'): + result = node.controller.step(buffer_states) + print(f" Controller {node_id} output: {result}") + + # If result has freq_correction, show it + if hasattr(result, 'freq_correction'): + print(f" freq_correction: {result.freq_correction}") + if hasattr(result, 'do_tick'): + print(f" do_tick: {result.do_tick}") + + # Simulate some buffer change + for buffer_name, buffer in node.buffers.items(): + # Add small random change to simulate traffic + change = np.random.randint(-2, 3) + new_occ = max(0, min(buffer.capacity, buffer.occupancy + change)) + buffer.occupancy = new_occ + + except Exception as e: + print(f" āŒ Error in controller {node_id}: {e}") + + except Exception as e: + print(f"āŒ Error during controller debugging: {e}") + import traceback + traceback.print_exc() + +def main(): + if len(sys.argv) < 2: + print("Usage: python debug_controller.py [steps]") + return + + config_path = sys.argv[1] + steps = int(sys.argv[2]) if len(sys.argv) > 2 else 10 + + debug_controller_step(config_path, steps) + +if __name__ == "__main__": + main() diff --git a/run_test.py b/run_test.py new file mode 100644 index 0000000..96b1074 --- /dev/null +++ b/run_test.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Simple test runner for debugging +""" +import sys +import os + +def run_test(config_name, duration=5.0): + """Run a simple test with the given config""" + config_path = f"configs/{config_name}" + + if not os.path.exists(config_path): + print(f"āŒ Config file not found: {config_path}") + return False + + print(f"šŸš€ Running test with {config_name} for {duration}s...") + + try: + # Import and run + import subprocess + result = subprocess.run([ + sys.executable, "System.py", + "--conf", config_path, + "--duration", str(duration) + ], capture_output=True, text=True, timeout=30) + + if result.returncode == 0: + print("āœ… Test completed successfully!") + print("šŸ“Š Output:") + print(result.stdout[-500:]) # Show last 500 chars + return True + else: + print("āŒ Test failed!") + print("šŸ“Š Error output:") + print(result.stderr) + return False + + except subprocess.TimeoutExpired: + print("ā° Test timed out (30s limit)") + return False + except Exception as e: + print(f"āŒ Error running test: {e}") + return False + +def list_configs(): + """List available configuration files""" + configs = [] + if os.path.exists("configs"): + for f in os.listdir("configs"): + if f.endswith(".json"): + configs.append(f) + return configs + +def main(): + print("šŸ”§ Bittide Debug Test Runner") + print("=" * 30) + + configs = list_configs() + if not configs: + print("āŒ No config files found in configs/") + return + + print("šŸ“‹ Available configurations:") + for i, config in enumerate(configs, 1): + print(f" {i}. {config}") + + if len(sys.argv) > 1: + config_name = sys.argv[1] + if not config_name.endswith('.json'): + config_name += '.json' + else: + print("\nšŸ” Select a configuration to test:") + try: + choice = int(input("Enter number: ")) - 1 + if 0 <= choice < len(configs): + config_name = configs[choice] + else: + print("āŒ Invalid choice") + return + except (ValueError, KeyboardInterrupt): + print("\nšŸ‘‹ Cancelled") + return + + duration = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0 + + success = run_test(config_name, duration) + + if success: + print(f"\nšŸŽ‰ Test with {config_name} completed successfully!") + else: + print(f"\nšŸ’„ Test with {config_name} failed!") + print("\nšŸ” Try debugging with:") + print(f" python debug_config.py configs/{config_name}") + print(f" python debug_controller.py configs/{config_name}") + +if __name__ == "__main__": + main()