Parameter Sweeps and Policy Comparison

The Python API makes it easy to run batch simulations, sweep parameters, and compare scheduling policies programmatically. Because each Engine is an independent object, you can run hundreds of simulations in a plain loop without any global state or process isolation.

Prerequisites

This tutorial assumes familiarity with the basics covered in Python Quick Start and Trace Analysis and Metrics.

Simulation state is not reusable

An Engine that has been run cannot be reset and run again. Always create a fresh ``Engine`` (and a fresh ``Platform``, ``Scheduler``, etc.) for each simulation run. The helper function pattern below enforces this naturally.

Helper Function

Encapsulate the full simulation setup in a single function that accepts the parameters you want to vary and returns a metrics object. This keeps each run isolated and the call site concise.

import pyschedsim as sim

def run_simulation(
    utilizations,        # list of per-task utilizations, e.g. [0.3, 0.25, 0.2, 0.15]
    reclamation="grub",  # "grub" or "cash"
    dvfs_policy=None,    # None, "pa", "ffa", or "csf"
    cooldown=0.001,      # DVFS cooldown in seconds
    cstate=1,            # target C-state for DPM
    seed=42,
):
    """Run one simulation and return the SimulationMetrics."""
    # ── Fresh engine and platform every call ──────────────────────────────
    engine   = sim.Engine()
    platform = engine.platform

    cpu   = platform.add_processor_type("cpu", 1.0)
    clock = platform.add_clock_domain(500.0, 2000.0)
    power = platform.add_power_domain([
        (0, 0, 0.0,   100.0),
        (1, 0, 0.001,  10.0),
    ])
    for _ in range(4):
        platform.add_processor(cpu, clock, power)

    # ── Generate scenario from per-task utilizations ──────────────────────
    # Each value must be in (0, 1].  Total may exceed 1 (multi-processor).
    scenario = sim.from_utilizations(utilizations, seed=seed)
    tasks    = sim.inject_scenario(engine, scenario)

    # Enable energy tracking before finalize (required for energy metrics)
    if dvfs_policy is not None:
        engine.enable_energy_tracking(True)

    platform.finalize()

    # Schedule job arrivals after inject and finalize
    for i, tp in enumerate(scenario.tasks):
        sim.schedule_arrivals(engine, tasks[i], tp.jobs)

    # ── Scheduler ─────────────────────────────────────────────────────────
    procs     = sim.get_all_processors(engine)
    scheduler = sim.EdfScheduler(engine, procs)
    for task in tasks:
        scheduler.add_server(task)

    # Reclamation policy (always required before DVFS)
    if reclamation == "grub":
        scheduler.enable_grub()
    elif reclamation == "cash":
        scheduler.enable_cash()

    # Optional DVFS/DPM policy
    if dvfs_policy == "pa":
        scheduler.enable_power_aware_dvfs(cooldown)
        scheduler.enable_basic_dpm(cstate)
    elif dvfs_policy == "ffa":
        scheduler.enable_ffa(cooldown, cstate)
    elif dvfs_policy == "csf":
        scheduler.enable_csf(cooldown, cstate)

    allocator = sim.SingleSchedulerAllocator(engine, scheduler)

    # ── Run ───────────────────────────────────────────────────────────────
    writer = sim.MemoryTraceWriter()
    engine.set_trace_writer(writer)
    engine.run()

    return writer.compute_metrics()

Note

sim.from_utilizations() generates a ScenarioData object with log-uniform periods and Weibull-distributed job execution times. Each element of utilizations must be in (0, 1]. The total utilization across all tasks may exceed 1.0 for multi-processor experiments.

Utilization Sweep

Vary total utilization from lightly loaded to near-capacity and observe how deadline miss rate evolves. The four-core platform has a capacity of 4.0; here we sweep from 1.0 to 3.95.

# Distribute total utilization evenly across 8 tasks.
# Each individual task utilization stays below 1.0.
total_utils = [1.0, 2.0, 3.0, 3.5, 3.8, 3.9, 3.95]
n_tasks = 8

print(f"{'Total U':>8}  {'Completed':>10}  {'Misses':>8}  {'Miss rate':>10}")
print("-" * 42)

for total_u in total_utils:
    per_task_u = total_u / n_tasks
    utils = [per_task_u] * n_tasks

    metrics = run_simulation(utils, reclamation="grub", seed=0)

    miss_rate = (metrics.deadline_misses / metrics.total_jobs
                 if metrics.total_jobs > 0 else 0.0)
    print(f"{total_u:>8.2f}  "
          f"{metrics.completed_jobs:>5}/{metrics.total_jobs:<5}  "
          f"{metrics.deadline_misses:>8}  "
          f"{miss_rate:>9.2%}")

Policy Comparison

For a fixed workload at moderate load, compare GRUB and CASH side by side.

n_tasks = 6
total_u = 3.0
per_task_u = total_u / n_tasks
utils = [per_task_u] * n_tasks

results = {}
for policy in ("grub", "cash"):
    results[policy] = run_simulation(utils, reclamation=policy, seed=42)

print(f"{'Policy':>6}  {'Completed':>10}  {'Misses':>8}  {'Preemptions':>12}")
print("-" * 44)
for policy, m in results.items():
    print(f"{policy:>6}  "
          f"{m.completed_jobs:>5}/{m.total_jobs:<5}  "
          f"{m.deadline_misses:>8}  "
          f"{m.preemptions:>12}")

DVFS Policy Comparison

Enable energy tracking and compare four energy management strategies for the same workload. The dvfs_policy=None baseline uses GRUB only, with no frequency scaling.

n_tasks = 6
utils = [0.45] * n_tasks  # total U = 2.7 on a 4-core platform

dvfs_configs = [
    ("No DVFS",       None),
    ("Power-Aware",   "pa"),
    ("FFA",           "ffa"),
    ("CSF",           "csf"),
]

print(f"{'Policy':>14}  {'Energy (mJ)':>12}  {'Misses':>8}  {'Freq changes':>13}")
print("-" * 54)

for label, dvfs in dvfs_configs:
    m = run_simulation(
        utils,
        reclamation="grub",
        dvfs_policy=dvfs,
        cooldown=0.001,
        seed=7,
    )
    print(f"{label:>14}  "
          f"{m.total_energy_mj:>12.2f}  "
          f"{m.deadline_misses:>8}  "
          f"{m.frequency_requests:>13}")

Note

total_energy_mj is 0.0 unless engine.enable_energy_tracking(True) is called before platform.finalize(). The helper function above handles this automatically when dvfs_policy is not None.

Collecting Results

For larger sweeps it is convenient to accumulate results in a list of dicts and format the table once at the end.

rows = []

seeds  = range(5)       # 5 independent runs per configuration
utils  = [0.4] * 8      # 8 tasks, total U = 3.2

for seed in seeds:
    for dvfs in (None, "ffa", "csf"):
        m = run_simulation(utils, reclamation="grub",
                           dvfs_policy=dvfs, seed=seed)
        rows.append({
            "seed":        seed,
            "dvfs":        dvfs or "none",
            "misses":      m.deadline_misses,
            "energy_mj":   m.total_energy_mj,
            "preemptions": m.preemptions,
        })

# Print a summary table
header = f"{'seed':>5}  {'dvfs':>6}  {'misses':>7}  {'energy_mJ':>10}  {'preempts':>9}"
print(header)
print("-" * len(header))
for r in rows:
    print(f"{r['seed']:>5}  {r['dvfs']:>6}  {r['misses']:>7}  "
          f"{r['energy_mj']:>10.2f}  {r['preemptions']:>9}")

Complete Example

A self-contained utilization sweep that compares GRUB with FFA and CSF, printing a formatted table for each total utilization level.

import pyschedsim as sim

def run_simulation(utilizations, reclamation="grub", dvfs_policy=None,
                   cooldown=0.001, cstate=1, seed=42):
    engine   = sim.Engine()
    platform = engine.platform
    cpu   = platform.add_processor_type("cpu", 1.0)
    clock = platform.add_clock_domain(500.0, 2000.0)
    power = platform.add_power_domain([
        (0, 0, 0.0, 100.0), (1, 0, 0.001, 10.0)
    ])
    for _ in range(4):
        platform.add_processor(cpu, clock, power)

    scenario = sim.from_utilizations(utilizations, seed=seed)
    tasks    = sim.inject_scenario(engine, scenario)

    if dvfs_policy is not None:
        engine.enable_energy_tracking(True)

    platform.finalize()

    for i, tp in enumerate(scenario.tasks):
        sim.schedule_arrivals(engine, tasks[i], tp.jobs)

    procs     = sim.get_all_processors(engine)
    scheduler = sim.EdfScheduler(engine, procs)
    for task in tasks:
        scheduler.add_server(task)

    if reclamation == "grub":
        scheduler.enable_grub()
    else:
        scheduler.enable_cash()

    if dvfs_policy == "pa":
        scheduler.enable_power_aware_dvfs(cooldown)
        scheduler.enable_basic_dpm(cstate)
    elif dvfs_policy == "ffa":
        scheduler.enable_ffa(cooldown, cstate)
    elif dvfs_policy == "csf":
        scheduler.enable_csf(cooldown, cstate)

    allocator = sim.SingleSchedulerAllocator(engine, scheduler)
    writer    = sim.MemoryTraceWriter()
    engine.set_trace_writer(writer)
    engine.run()
    return writer.compute_metrics()


total_utils = [1.0, 2.0, 3.0, 3.5, 3.8, 3.9]
n_tasks     = 8
dvfs_labels = [("GRUB",     None),
               ("GRUB+FFA", "ffa"),
               ("GRUB+CSF", "csf")]

# Header
col = 12
header = f"{'Total U':>8}" + "".join(
    f"  {lbl+' misses':>{col}}  {lbl+' mJ':>{col}}"
    for lbl, _ in dvfs_labels
)
print(header)
print("-" * len(header))

for total_u in total_utils:
    per_task_u = total_u / n_tasks
    utils = [per_task_u] * n_tasks
    row = f"{total_u:>8.2f}"
    for label, dvfs in dvfs_labels:
        m = run_simulation(utils, dvfs_policy=dvfs, seed=0)
        miss_rate = (m.deadline_misses / m.total_jobs
                     if m.total_jobs > 0 else 0.0)
        row += f"  {miss_rate:>{col}.2%}  {m.total_energy_mj:>{col}.1f}"
    print(row)

Next Steps