Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[]))
Expand Down
126 changes: 126 additions & 0 deletions Controllers/FuzzyPController.py
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions Controllers/FuzzyPIController.py
Original file line number Diff line number Diff line change
@@ -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
119 changes: 100 additions & 19 deletions DelayGenerator.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion Node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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###
Expand Down
Loading