add starting point for mosis 2024 assignment

This commit is contained in:
Joeri Exelmans 2024-12-13 11:58:27 +01:00
parent 3404c782a9
commit 4b959bc98b
9 changed files with 441 additions and 0 deletions

2
.gitignore vendored
View file

@ -10,3 +10,5 @@
*build* *build*
test/output/* test/output/*
/.idea/* /.idea/*
assignment_output/

1
assignment/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*_solution.py

24
assignment/TIPS.txt Normal file
View file

@ -0,0 +1,24 @@
CODING CONVENTIONS
1. write your methods always in the following order:
extTransition
timeAdvance
outputFnc
intTransition
this reflects the order in which the methods are called by the simulator:
extTransition always has highest priority (can interrupt anything)
timeAdvance is called before outputFnc
outputFnc is called right before intTransition
2. input/output port attributes start with 'in_' and 'out_'
TROUBLESHOOTING
- did you forget to return `self.state` from intTransition or extTransition ?
- did you accidentally write to `self.x` instead of `self.state.x` ?
- did you modify the state in timeAdvance or outputFnc (NOT ALLOWED!!)

91
assignment/atomicdevs.py Normal file
View file

@ -0,0 +1,91 @@
### EDIT THIS FILE ###
from pypdevs.DEVS import AtomicDEVS
from environment import *
import random
import dataclasses
class Queue(AtomicDEVS):
def __init__(self, ship_sizes):
super().__init__("Queue")
# self.state = QueueState(...)
# def extTransition(self, inputs):
# pass
# def timeAdvance(self):
# pass
# def outputFnc(self):
# pass
# def intTransition(self):
# pass
PRIORITIZE_BIGGER_SHIPS = 0
PRIORITIZE_SMALLER_SHIPS = 1
class RoundRobinLoadBalancer(AtomicDEVS):
def __init__(self,
lock_capacities=[3,2], # two locks of capacities 3 and 2.
priority=PRIORITIZE_BIGGER_SHIPS,
):
super().__init__("RoundRobinLoadBalancer")
# self.state = LoadBalancerState(...)
# def extTransition(self, inputs):
# pass
# def timeAdvance(self):
# pass
# def outputFnc(self):
# pass
# def intTransition(self):
# pass
class FillErUpLoadBalancer(AtomicDEVS):
def __init__(self,
lock_capacities=[3,2], # two locks of capacities 3 and 2.
priority=PRIORITIZE_BIGGER_SHIPS,
):
super().__init__("FillErUpLoadBalancer")
# self.state = LoadBalancerState(...)
# def extTransition(self, inputs):
# pass
# def timeAdvance(self):
# pass
# def outputFnc(self):
# pass
# def intTransition(self):
# pass
class Lock(AtomicDEVS):
def __init__(self,
capacity=2, # lock capacity (2 means: 2 ships of size 1 will fit, or 1 ship of size 2)
max_wait_duration=60.0,
passthrough_duration=60.0*15.0, # how long does it take for the lock to let a ship pass through it
):
super().__init__("Lock")
# self.state = LockState(...)
# def extTransition(self, inputs):
# pass
# def timeAdvance(self):
# pass
# def outputFnc(self):
# pass
# def intTransition(self):
# pass
### EDIT THIS FILE ###

96
assignment/environment.py Normal file
View file

@ -0,0 +1,96 @@
### DO NOT EDIT THIS FILE ###
from pypdevs.DEVS import AtomicDEVS
import random
import dataclasses
# The reason for annotating the *State-classes as 'dataclass', is because this automatically generates a nice __repr__-function, so that if the simulator is set to verbose, you can actually see what the state is.
class Ship:
def __init__(self, size, creation_time):
self.size = size
self.creation_time = creation_time
# useful in verbose mode:
def __repr__(self):
return f"Ship(size={self.size},created={self.creation_time})"
@dataclasses.dataclass
class GeneratorState:
current_time: float
time_until_next_ship: float
to_generate: int
random: random.Random
def __init__(self, seed=0, gen_num=1000):
self.current_time = 0.0 # for statistics only
self.time_until_next_ship = 0.0
self.to_generate = gen_num
self.random = random.Random(seed)
class Generator(AtomicDEVS):
def __init__(self,
seed=0, # random seed
lambd=1.0/60.0, # how often to generate a ship - in this example, once per minute
gen_types=[1,1,2], # ship sizes to generate, will be sampled uniformly - in this example, size 1 is twice as likely as size 2.
gen_num=1000, # number of ships total to generate
):
super().__init__("Generator")
# State (for everything that is mutable)
self.state = GeneratorState(seed=seed, gen_num=gen_num)
# I/O
self.out_ship = self.addOutPort("out_event")
# Parameters (read-only)
self.lambd = lambd
self.gen_types = gen_types
def timeAdvance(self):
return self.state.time_until_next_ship
def outputFnc(self):
size = self.state.random.choice(self.gen_types) # uniformly sample from gen_types
# watch out: outputFnc is called *before* intTransition!
creation = self.state.current_time + self.state.time_until_next_ship
return { self.out_ship: Ship(size, creation) }
def intTransition(self):
self.state.current_time += self.state.time_until_next_ship
self.state.to_generate -= 1
if self.state.to_generate > 0:
self.state.time_until_next_ship = self.state.random.expovariate(self.lambd)
else:
# stop generating
self.state.time_until_next_ship = float('inf')
return self.state
@dataclasses.dataclass
class SinkState:
current_time: float
ships: list
def __init__(self):
self.current_time = 0.0
self.ships = []
class Sink(AtomicDEVS):
def __init__(self):
super().__init__("Sink")
self.state = SinkState()
self.in_ships = self.addInPort("in_ships")
def extTransition(self, inputs):
self.state.current_time += self.elapsed
if self.in_ships in inputs:
ships = inputs[self.in_ships]
for ship in ships:
ship.finished_time = self.state.current_time
# amount of time spent in the system:
ship.queueing_duration = ship.finished_time - ship.creation_time
self.state.ships.extend(ships)
return self.state
### DO NOT EDIT THIS FILE ###

View file

@ -0,0 +1,56 @@
def make_plot_ships_script(priority:str, strategy:str, max_waits:list[float], gen_num:int):
return (f"""
### priority={priority}, strategy={strategy} ###
set terminal svg
# plot 1. x-axis: ships, y-axis: queuing duration of ship
set out 'plot_ships_{strategy}_{priority}.svg'
set title "Queueing duration"
set xlabel "Ship #"
set ylabel "Seconds"
#unset xlabel
#unset xtics
set key title "Max Wait"
set key bottom center out
set key horizontal
"""
# + '\n'.join([
# f"set style line {i+1} lw 4"
# for i in range(len(max_waits))
# ])
+ f"""
# set yrange [0:90000]
set xrange [0:{gen_num}]
set style fill solid
plot 'output_{strategy}_{priority}.csv' \\\n """ + ", \\\n '' ".join([
f"using 1:{i+1} title '{max_wait}' w boxes ls {i+1}"
for i, max_wait in enumerate(max_waits)
]))
def make_plot_box_script(priority:str, strategy:str, max_waits:list[float], gen_num:int):
return (f"""
# plot 2. x-axis: max-wait parameter, y-axis: queueing durations of ships
set out 'plot_box_{strategy}_{priority}.svg'
set style fill solid 0.25 border -1
set style boxplot outliers pointtype 7
set style data boxplot
set key off
set xlabel "Max Wait"
unset xrange
unset yrange
set xtics (""" + ', '.join([ f"'{max_wait}' {i}"
for i, max_wait in enumerate(max_waits)]) + f""")
plot 'output_{strategy}_{priority}.csv' \\\n """ + ", \\\n '' ".join([
f"using ({i}):{i+2} title '{max_wait}'"
for i, max_wait in enumerate(max_waits)
]))

109
assignment/runner.py Normal file
View file

@ -0,0 +1,109 @@
import os
from pypdevs.simulator import Simulator
from plot_template import make_plot_ships_script, make_plot_box_script
# from system_solution import * # Teacher's solution
from system import *
## Parameters ##
gen_num = 500 # how many ships to generate
# How often to generate a ship (on average)
gen_rate = 1/60/4 # once every 4 minutes
# Ship size will be sampled uniformly from the following list.
gen_types = [1,1,2] # ship size '1' twice as likely to be generated as ship size '2'
# Load balancer...
priorities = {
# you can outcomment one of these lines to reduce the number of experiments (useful for debugging):
PRIORITIZE_BIGGER_SHIPS: "bigger",
PRIORITIZE_SMALLER_SHIPS: "smaller",
}
strategies = {
# you can outcomment one of these lines to reduce the number of experiments (useful for debugging):
STRATEGY_ROUND_ROBIN: "roundrobin",
STRATEGY_FILL_ER_UP: "fillerup",
}
# The number of locks and their capacities
lock_capacities=[3,2] # two locks, of capacity 3 and 2
# The different parameters to try for lock_max_wait
lock_max_waits = [ 0.0+i*120.0 for i in range(5) ] # all these values will be attempted
# lock_max_waits = [ 15.0 ] # <-- uncomment if you only want to run an experiment with this value (useful for debugging)
# How long does it take for a ship to pass through a lock
passthrough_duration = 60.0*15 # 15 minutes
outdir = "assignment_output"
plots_ships = []
plots_box = []
os.makedirs(outdir, exist_ok=True)
# try all combinations of priorities and strategies (4 total)
for priority in priorities:
for strategy in strategies:
values = []
# and in each experiment, try a bunch of different values for the 'lock_max_wait' parameter:
for lock_max_wait in lock_max_waits:
print("Run simulation:", priorities[priority], strategies[strategy], "max_wait =",lock_max_wait)
sys = LockQueueingSystem(
# See system.py for explanation of these values:
seed=0,
gen_num=gen_num,
gen_rate=gen_rate,
gen_types=gen_types,
load_balancer_strategy=strategy,
lock_capacities=lock_capacities,
priority=priority,
lock_max_wait=lock_max_wait,
passthrough_duration=passthrough_duration,
)
sim = Simulator(sys)
sim.setClassicDEVS()
# sim.setVerbose() # <-- uncomment to see what's going on
sim.simulate()
# all the ships that made it through
ships = sys.sink.state.ships
values.append([ship.queueing_duration for ship in ships])
# Write out all the ship queueuing durations for every 'lock_max_wait' parameter
# for every ship, we write a line:
# <ship_num>, time_max_wait0, time_max_wait1, time_max_wait2, ... time_max_wait10
filename = f'{outdir}/output_{strategies[strategy]}_{priorities[priority]}.csv'
with open(filename, 'w') as f:
try:
for i in range(gen_num):
f.write("%s" % i)
for j in range(len(values)):
f.write(", %5f" % (values[j][i]))
f.write("\n")
except IndexError as e:
raise Exception("There was an IndexError, meaning that fewer ships have made it to the sink than expected.\nYour model is not (yet) correct.") from e
# Generate gnuplot code:
plots_ships.append(make_plot_ships_script(
priority=priorities[priority],
strategy=strategies[strategy],
max_waits=lock_max_waits,
gen_num=gen_num,
))
plots_box.append(make_plot_box_script(
priority=priorities[priority],
strategy=strategies[strategy],
max_waits=lock_max_waits,
gen_num=gen_num,
))
# Finally, write out a single gnuplot script that plots everything
with open(f'{outdir}/plot.gnuplot', 'w') as f:
# first plot the ships
f.write('\n\n'.join(plots_ships))
# then do the box plots
f.write('\n\n'.join(plots_box))

61
assignment/system.py Normal file
View file

@ -0,0 +1,61 @@
### EDIT THIS FILE ###
from pypdevs.DEVS import CoupledDEVS
from atomicdevs import *
STRATEGY_ROUND_ROBIN = 0
STRATEGY_FILL_ER_UP = 1
class LockQueueingSystem(CoupledDEVS):
def __init__(self,
# See runner.py for an explanation of these parameters!!
seed,
gen_num,
gen_rate,
gen_types,
load_balancer_strategy,
lock_capacities,
priority,
lock_max_wait,
passthrough_duration,
):
super().__init__("LockQueueingSystem")
# Instantiate sub-models with the right parameters, and add them to the CoupledDEVS:
generator = self.addSubModel(Generator(
seed=seed, # random seed
lambd=gen_rate,
gen_types=gen_types,
gen_num=gen_num,
))
if load_balancer_strategy == STRATEGY_ROUND_ROBIN:
LoadBalancer = RoundRobinLoadBalancer
elif load_balancer_strategy == STRATEGY_FILL_ER_UP:
LoadBalancer = FillErUpLoadBalancer
load_balancer = self.addSubModel(LoadBalancer(
lock_capacities=lock_capacities,
priority=priority,
))
locks = [ self.addSubModel(Lock(
capacity=lock_capacity,
max_wait_duration=lock_max_wait,
passthrough_duration=passthrough_duration))
for lock_capacity in lock_capacities ]
sink = self.addSubModel(Sink())
# Don't forget to connect the input/output ports of the different sub-models:
# for instance:
# self.connectPorts(generator.out_ship, queue.in_ship)
# ...
# Our runner.py script needs access to the 'sink'-state after completing the simulation:
self.sink = sink
### EDIT THIS FILE ###

View file

@ -30,6 +30,7 @@ for i in range(1, max_processors):
# PythonPDEVS specific setup and configuration # PythonPDEVS specific setup and configuration
sim = Simulator(m) sim = Simulator(m)
sim.setClassicDEVS() sim.setClassicDEVS()
# sim.setVerbose() # <- uncomment to see what's going on
sim.simulate() sim.simulate()
# Gather information for output # Gather information for output