diff --git a/.gitignore b/.gitignore index afc9798..19078ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tests/.tmp __pycache__ .vscode .ruff_cache +solution.csv diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2a59140 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,126 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: 1-tiny-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/1-tiny-test-case/architecture.csv", + "data/testcases/1-tiny-test-case/budgets.csv", + "data/testcases/1-tiny-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 2-small-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/2-small-test-case/architecture.csv", + "data/testcases/2-small-test-case/budgets.csv", + "data/testcases/2-small-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 3-medium-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/3-medium-test-case/architecture.csv", + "data/testcases/3-medium-test-case/budgets.csv", + "data/testcases/3-medium-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 4-large-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/4-large-test-case/architecture.csv", + "data/testcases/4-large-test-case/budgets.csv", + "data/testcases/4-large-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 5-huge-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/5-huge-test-case/architecture.csv", + "data/testcases/5-huge-test-case/budgets.csv", + "data/testcases/5-huge-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 6-gigantic-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/6-gigantic-test-case/architecture.csv", + "data/testcases/6-gigantic-test-case/budgets.csv", + "data/testcases/6-gigantic-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 7-unschedulable-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/7-unschedulable-test-case/architecture.csv", + "data/testcases/7-unschedulable-test-case/budgets.csv", + "data/testcases/7-unschedulable-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 8-unschedulable-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/8-unschedulable-test-case/architecture.csv", + "data/testcases/8-unschedulable-test-case/budgets.csv", + "data/testcases/8-unschedulable-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 9-unschedulable-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/9-unschedulable-test-case/architecture.csv", + "data/testcases/9-unschedulable-test-case/budgets.csv", + "data/testcases/9-unschedulable-test-case/tasks.csv" + ] + }, + { + "name": "Python Debugger: 10-unschedulable-test-case", + "type": "debugpy", + "request": "launch", + "program": "src/analysis.py", + "console": "integratedTerminal", + "args": [ + "data/testcases/10-unschedulable-test-case/architecture.csv", + "data/testcases/10-unschedulable-test-case/budgets.csv", + "data/testcases/10-unschedulable-test-case/tasks.csv" + ] + } + + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1ad1dee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "idf.pythonInstallPath": "/usr/bin/python3" +} \ No newline at end of file diff --git a/data/testcases b/data/testcases index d255eae..0166426 160000 --- a/data/testcases +++ b/data/testcases @@ -1 +1 @@ -Subproject commit d255eae14fd7153234d504e2212788a06580efbe +Subproject commit 0166426bc193a61c1eba36877ea93511ddf7db04 diff --git a/src/analysis.py b/src/analysis.py index a3833a5..564c68e 100644 --- a/src/analysis.py +++ b/src/analysis.py @@ -1,11 +1,208 @@ -from common.csvreader import read_csv import sys +import csv +import math +from functools import reduce + +from common.csvreader import read_csv +from common.DBF import DBF +from common.BDR import BDR +from common.scheduler import Scheduler + + +def lcm(a: int, b: int) -> int: + """Compute least common multiple of two integers.""" + return abs(a * b) // math.gcd(a, b) + + +def adjust_wcet(tasks, budgets, architectures): + # Adjust WCET by core speed factor (scaling per architecture) + core_speed_map = {arch.core_id: arch.speed_factor for arch in architectures} + component_core_map = {budget.component_id: budget.core_id for budget in budgets} + for task in tasks: + core_id = component_core_map[task.component_id] + task.wcet = task.wcet / core_speed_map[core_id] + return tasks + + +def group_tasks_by_component(tasks, budgets): + # Group tasks into their components based on budgets.csv + components = {} + for budget in budgets: + comp_id = budget.component_id + comp_tasks = [t for t in tasks if t.component_id == comp_id] + # Sort for RM priority order + if budget.scheduler == Scheduler.RM: + comp_tasks.sort(key=lambda t: t.priority) + components[comp_id] = { + 'tasks': comp_tasks, + 'scheduler': budget.scheduler, + 'core_id': budget.core_id, + 'budget': budget.budget, # Q from PRM + 'period': budget.period, # P from PRM + 'schedulable': False, + } + return components + + +def check_component_schedulability(components): + """ + For each component, check local schedulability under its PRM budget: + - Convert PRM (Q,P) to a conservative BDR lower-bound via Half-Half (Theorem 3): rate=Q/P, delay=2*(P−Q) + - Use Supply Bound Function sbf_BDR (Eq. 6) for supply.sbf(t) + - Use Demand Bound Functions: + • RM: dbf_rm(W,t,i) (Eq. 4) + • EDF: dbf_edf(W,t) (Eq. 2) + - For both schedulers, generate all critical points t = k·T_j up to each component's max deadline. + Then apply the tests: + RM: ∀τ_i ∃ t ≤ T_i such that dbf_rm(W,t,i) ≤ sbf(t) + EDF: ∀ t ≥ 0 dbf_edf(W,t) ≤ sbf(t) + """ + for comp in components.values(): + tasks = comp['tasks'] + sched = comp['scheduler'] + Q, P = comp['budget'], comp['period'] + supply = BDR(rate=Q/P, delay=2*(P-Q)) # Theorem 3 + + # Build global critical points: multiples of all periods + periods = [t.period for t in tasks] + max_deadline = max(periods) if periods else 0 + time_points = set() + for T in periods: + k = 1 + while k * T <= max_deadline: + time_points.add(k * T) + k += 1 + time_points = sorted(time_points) + + ok = True + if sched == Scheduler.RM: + # For each task i, need ∃ t ≤ T_i s.t. dbf_rm(W,t,i) ≤ sbf(t) + for idx, task in enumerate(tasks): + Ti = task.period + found = False + for t in time_points: + if t > Ti: + break + demand = DBF.dbf_rm(tasks, t, idx) # Eq.4 + if demand <= supply.sbf(t): # Eq.6 + found = True + break + if not found: + ok = False + break + else: + # EDF: ∀ t, dbf_edf(W,t) ≤ sbf(t) + for t in time_points: + demand = DBF.dbf_edf(tasks, t) # Eq.2 + if demand > supply.sbf(t): # Eq.6 + ok = False + break + + # Also ensure every individual task meets its deadline under this supply + task_checks = [] + for idx, task in enumerate(tasks): + if sched == Scheduler.RM: + demand = DBF.dbf_rm(tasks, task.period, idx) + else: + demand = DBF.dbf_edf(tasks, task.period) + task_checks.append(demand <= supply.sbf(task.period)) + comp['schedulable'] = ok and all(task_checks) + return components + + +def summarize_by_core(components, architectures): + """ + At system level, apply Theorem 1 for BDR composition via can_schedule_children: + - Parent = full-CPU BDR(rate=1.0, delay=0.0) + - Children = list of BDR(rate=Q/P, delay=2*(P-Q)) for each component on the core + - Also ensure each component passed its local schedulability check + """ + # Group components per core + core_map = {arch.core_id: [] for arch in architectures} + for comp in components.values(): + core_map[comp['core_id']].append(comp) + + core_summary = {} + for core_id, comps_on_core in core_map.items(): + # Build BDR interfaces for each child component + child_bdrs = [BDR(rate=comp['budget']/comp['period'], + delay=2*(comp['period']-comp['budget'])) + for comp in comps_on_core] + # Parent BDR representing full CPU + parent_bdr = BDR(rate=1.0, delay=0.0) + + # Theorem 1: compositional check using helper + # Special-case: if parent delay == 0, allow child.delay >= 0 + if parent_bdr.delay == 0.0: + compositional_ok = sum(c.rate for c in child_bdrs) <= parent_bdr.rate + else: + compositional_ok = BDR.can_schedule_children(parent_bdr, child_bdrs) + + # Ensure each component's own schedulability + all_children_ok = all(comp['schedulable'] for comp in comps_on_core) + + core_summary[core_id] = compositional_ok and all_children_ok + return core_summary + + +def output_report(components, core_summary): + # Produce console output only (no file) + for comp_id, comp in components.items(): + Q, P = comp['budget'], comp['period'] + rate = Q/P # PRM bandwidth + delay = 2*(P-Q) # BDR startup delay + label = 'Schedulable' if comp['schedulable'] else 'Not schedulable' + print(f"Component {comp_id} (Core {comp['core_id']}, {comp['scheduler'].name}): " + f"PRM_sup=(Q={Q},P={P}), BLB(rate={rate:.4f},delay={delay:.2f}) - {label}") + + print('\nCore-level Summary:') + for core_id, ok in core_summary.items(): + core_stat = 'Schedulable' if ok else 'Not schedulable' + print(f"Core {core_id}: {core_stat}") + + +def write_solution_csv(tasks, components, filename='solution.csv'): # noqa: E302(tasks, components, filename='solution.csv'):(tasks, components, filename='solution.csv'): + # CSV with task- and component-level results + rows = [] + for cid, comp in components.items(): + supply = BDR(rate=comp['budget']/comp['period'], delay=2*(comp['period']-comp['budget'])) + for task in comp['tasks']: + if comp['scheduler'] == Scheduler.RM: + idx = comp['tasks'].index(task) + demand = DBF.dbf_rm(comp['tasks'], task.period, idx) # Eq.4 + else: + demand = DBF.dbf_edf(comp['tasks'], task.period) # Eq.2 + rows.append({ + 'task_name': task.task_name, + 'component_id': cid, + 'task_schedulable': int(demand <= supply.sbf(task.period)), + 'component_schedulable': int(comp['schedulable']) + }) + with open(filename, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=[ + 'task_name','component_id','task_schedulable','component_schedulable' + ]) + writer.writeheader() + for r in rows: + writer.writerow(r) + def main(): - architecture, budgets, tasks = read_csv() + if len(sys.argv) != 4: + print("Usage: python analysis.py ") + sys.exit(1) + architectures, budgets, tasks = read_csv() + tasks = adjust_wcet(tasks, budgets, architectures) + components = group_tasks_by_component(tasks, budgets) + # Local component checks + components = check_component_schedulability(components) + # Global core summaries + core_summary = summarize_by_core(components, architectures) + output_report(components, core_summary) + write_solution_csv(tasks, components) -if __name__ == "__main__": - main() \ No newline at end of file +if __name__ == '__main__': + main() diff --git a/src/common/BDR.py b/src/common/BDR.py index 05778f8..dbee84c 100644 --- a/src/common/BDR.py +++ b/src/common/BDR.py @@ -1,139 +1,65 @@ -from SRPModel import SRPModel -import numpy as np +# common/BDR.py + +import math +from typing import List, Tuple +from functools import reduce + +from common.DBF import DBF +from common.scheduler import Scheduler + + +def lcm(a: int, b: int) -> int: + """Compute least common multiple of two integers.""" + return abs(a * b) // math.gcd(a, b) + class BDR: - def __init__(self, model: SRPModel, time_interval: int,): - self.model = model - self.time_interval = time_interval - self.availability_factor = self.get_availability_factor() - self.supply_function = self.get_supply_function() - self.supply_bound_function = self.get_sbfSPR(self.supply_function, time_interval) - self.partition_delay = self.get_partition_delay() - self.sbf_bdr = self.get_sbfBDR() - def get_availability_factor(self) -> float: + def __init__(self, rate: float, delay: float): """ - Calculate the availability factor for a given SRP model. - - Args: - model (SRPModel): The SRP model to calculate the availability factor for. - - Returns: - float: The availability factor for the given SRP model. + Bounded-Delay Resource model: + rate – long-term supply rate (0 < rate <= 1) + delay – maximum startup delay """ - # Calculate the total time period - total_time_period = self.model.period - - # Calculate the total resource access time - total_resource_access_time = sum(end - start for start, end in self.model.resource_access) - - # Calculate the availability factor - availability_factor = total_resource_access_time / total_time_period - - return availability_factor + self.rate = rate + self.delay = delay - def get_partition_delay(self) -> float: + def sbf(self, interval: float) -> float: """ - Calculate the partition delay for a given SRP model. - - Args: - model (SRPModel): The SRP model to calculate the partition delay for. - - Returns: - float: The partition delay for the given SRP model. + Supply Bound Function (Eq. 6): + sbf(interval) = 0, if interval < delay + rate * (interval - delay), otherwise """ - partition_delay = 0 - supply_bound_function = self.supply_bound_function - for time, supply in enumerate(supply_bound_function): - if self.availability_factor < supply: - slope = (supply - supply_bound_function[time - 1]) / 1 - intercept = supply - slope * time - partition_delay = (self.availability_factor - intercept) / slope - break - - return partition_delay + if interval < self.delay: + return 0.0 + return self.rate * (interval - self.delay) - def get_supply_function(self) -> dict[int, list[int]]: + @staticmethod + def can_schedule_children(parent: "BDR", children: List["BDR"]) -> bool: """ - Calculate the supply function for a given SRP model. - - Args: - model (SRPModel): The SRP model to calculate the supply function for. - time_interval (int): The time interval to calculate the supply function over. - - Returns: - dict[int, list[int]]: The supply function for the given SRP model. + Theorem 1: A parent BDR can host these child interfaces iff + 1) sum(child.rate) <= parent.rate + 2) child.delay > parent.delay for every child """ - supply_function: dict[int, list[int]] = { - end: [0 for _ in range(self.time_interval)] for start, end in self.model.resource_access - } - starting_time = [end for start, end in self.model.resource_access] - - for end_key in starting_time: - period_tracker = end_key - 1 - for time in range(self.time_interval): - period_tracker += 1 - if period_tracker == self.model.period: - period_tracker = 0 - supply_function[end_key][time] = supply_function[end_key][time - 1] - for start, end in self.model.resource_access: - if start < period_tracker <= end and time != 0: - supply_function[end_key][time] = supply_function[end_key][time - 1] + 1 - break - - return supply_function + if sum(c.rate for c in children) > parent.rate: + return False + if any(c.delay <= parent.delay for c in children): + return False + return True - def get_sbfSPR(self, supply_function: dict[int, list[int]], time_interval: int) -> list[int]: + def supply_task_params(self) -> Tuple[float, float]: """ - Calculate the supply bound function for a given supply function. - - Args: - supply_function (dict[int, list[int]]): The supply function to calculate the supply bound function for. - - Returns: - list[int]: The supply bound function for the given supply function. + Theorem 3 (Half-Half): Transform this BDR interface into a periodic supply task. + Returns (budget, period): + period = delay / (2 * (1 - rate)) + budget = rate * period + Special-case: if rate == 1.0, returns (1.0, 1.0) for full CPU. """ - supply_function = self.supply_function - values = supply_function.values() - supply_bound_function = [0 for _ in range(self.time_interval)] - for i in range(self.time_interval): - supply_bound_function[i] = min([value[i] for value in values]) - return supply_bound_function - - def get_sbfBDR(self) -> list[int]: - sbf = [] - time_interval = np.linspace(0, self.time_interval, 100) - print(time_interval) - for i in time_interval: - value = self.availability_factor*(i - self.partition_delay) - if value <= 0: - sbf.append(0) - else: - sbf.append(value) - return sbf - - - -if __name__ == "__main__": - # Example usage - model = SRPModel(resource_access=[(1, 2), (5, 7)], period=8) - bdr = BDR(model=model, time_interval=26) - - print("Availability Factor:", bdr.get_availability_factor()) - print("Partition Delay:", bdr.get_partition_delay()) - print("Supply Function:", bdr.get_supply_function()) - print("Supply Bound Function:", bdr.get_sbfSPR(bdr.get_supply_function(), 20)) - import matplotlib.pyplot as plt + if self.rate >= 1.0: + return 1.0, 1.0 + if self.rate == 0.0: + return self.rate, 0.0 + denom = 2.0 * (1.0 - self.rate) + period = self.delay / denom + budget = self.rate * period + return budget, period - # Plot the supply function - supply_function = bdr.get_supply_function() - for end_key, values in supply_function.items(): - plt.plot(range(bdr.time_interval), values, label=f"End Key {end_key}") - plt.plot(range(bdr.time_interval), bdr.get_sbfSPR(supply_function, bdr.time_interval), label="Supply Bound Function", linestyle='--') - plt.plot(bdr.partition_delay, bdr.availability_factor, 'ro', label="Partition Delay") - plt.plot(np.linspace(0, 26, 100), bdr.sbf_bdr, label="SBF BDR", linestyle='--') - plt.title("Supply Function") - plt.xlabel("Time") - plt.ylabel("Supply") - plt.legend() - plt.grid(True) - plt.show() \ No newline at end of file diff --git a/src/common/DBF.py b/src/common/DBF.py index ee8d2cc..4c1daa4 100644 --- a/src/common/DBF.py +++ b/src/common/DBF.py @@ -1,53 +1,46 @@ -from abc import ABC, abstractmethod -from common.task import Task +# common/DBF.py -class DBF(): - def __init__(self, tasks: list[Task], time_interval: float, explicit_dead_line: float = 0): - self.explicit_dead_line = explicit_dead_line - self.tasks = tasks - self.time_interval = time_interval - @abstractmethod - def getDBS(self) -> float: ... - -class DBF_EDF(DBF): - def __init__(self, tasks: list[Task], time_interval: float, explicit_dead_line: float = 0): - super().__init__(tasks, time_interval, explicit_dead_line) - - def getDBS(self) -> float: +import math +from typing import Sequence + +class DBF: + @staticmethod + def dbf_edf(tasks: Sequence, interval: float) -> float: + """ + Demand Bound Function under EDF for implicit-deadline tasks: + dbfEDF(W, interval) = sum(floor(interval / period) * wcet) """ - Calculate the Demand Bound Function (DBF) for a set of tasks. - The DBF is computed as the sum of the workload contributions of all tasks - within a specified time interval. Each task contributes to the DBF based - on its worst-case execution time (WCET) and period. - Returns: - float: The computed DBF value for the set of tasks. + total_demand = 0.0 + for task in tasks: + executions = math.floor(interval / task.period) + total_demand += executions * task.wcet + return total_demand + + @staticmethod + def dbf_edf_explicit(tasks: Sequence, interval: float) -> float: """ - - dbs: float = 0 - for task in self.tasks: - num = (self.time_interval + (task.period * self.explicit_dead_line) - self.explicit_dead_line) - dbs += (num / task.period) * task.wcet - return dbs - -class DBF_FPS(DBF): - def __init__(self, tasks: list[Task], time_interval: float, task_index: int): - self.task_index = task_index - super().__init__(tasks, time_interval) - - def getDBS(self) -> float: + Demand Bound Function under EDF for explicit-deadline tasks: + dbfEDF(W, interval) = sum(floor((interval + period - deadline) / period) * wcet) + """ + total_demand = 0.0 + for task in tasks: + deadline = getattr(task, 'deadline', task.period) + job_count = math.floor((interval + task.period - deadline) / task.period) + if job_count > 0: + total_demand += job_count * task.wcet + return total_demand + + @staticmethod + def dbf_rm(tasks: Sequence, interval: float, index: int) -> float: """ - Calculate the Demand Bound Function (DBF) for a set of tasks. - The DBF is computed as the sum of the workload contributions of all tasks - within a specified time interval. Each task contributes to the DBF based - on its worst-case execution time (WCET) and period. - Returns: - float: The computed DBF value for the set of tasks. + Demand Bound Function under RM/DM for the task at 'index': + dbfRM(W, interval, i) = wcet_i + sum(ceil(interval / period_k) * wcet_k) + for all tasks k with higher priority (lower index). """ - - my_task = self.tasks[self.task_index] - higher_priority_tasks = [task for task in self.tasks if task.priority > self.tasks[self.task_index].priority] - dbs: float = my_task.wcet - for task in higher_priority_tasks: - dbs += (self.time_interval / task.period) * task.wcet - return dbs - \ No newline at end of file + # demand from the task itself + demand = tasks[index].wcet + # plus interference from all higher-priority tasks + for higher in tasks[:index]: + invocations = math.ceil(interval / higher.period) + demand += invocations * higher.wcet + return demand diff --git a/src/common/budget.py b/src/common/budget.py index 0e6a6fb..8cd2596 100644 --- a/src/common/budget.py +++ b/src/common/budget.py @@ -6,7 +6,7 @@ class Budget: component_id: str scheduler:Scheduler - budget:int - period:int + budget:float + period:float core_id:int priority:int|None diff --git a/src/common/csvreader.py b/src/common/csvreader.py index 9b389fb..5c36230 100644 --- a/src/common/csvreader.py +++ b/src/common/csvreader.py @@ -61,8 +61,8 @@ def read_tasks(csv:str)-> list[Task]: for _,row in df.iterrows(): task = Task( task_name=row['task_name'], - wcet=row['wcet'], - period=row['period'], + wcet=(row['wcet']), + period=(row['period']), component_id=row['component_id'], priority=row['priority'], ) @@ -84,12 +84,6 @@ def read_csv() -> tuple[list[Architecture], list[Budget], list[Task]]: budgets = read_budgets(budget_file) tasks = read_tasks(tasks_file) - print(f"Successfully read architectures: {architectures}") - print(f"Successfully read budgets: {budgets}") - print(f"Successfully read tasks: {tasks}") - - # Add your analysis code here - except FileNotFoundError as e: print(f"Error: File not found - {e}") sys.exit(1) diff --git a/src/common/task.py b/src/common/task.py index 93fe38e..b92f52a 100644 --- a/src/common/task.py +++ b/src/common/task.py @@ -3,8 +3,8 @@ @dataclass class Task: task_name:str - wcet:int - period:int + wcet:float + period:float component_id:str priority:int|None