add starting point for mosis 2024 assignment
This commit is contained in:
parent
3404c782a9
commit
4b959bc98b
9 changed files with 441 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -10,3 +10,5 @@
|
||||||
*build*
|
*build*
|
||||||
test/output/*
|
test/output/*
|
||||||
/.idea/*
|
/.idea/*
|
||||||
|
|
||||||
|
assignment_output/
|
||||||
1
assignment/.gitignore
vendored
Normal file
1
assignment/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*_solution.py
|
||||||
24
assignment/TIPS.txt
Normal file
24
assignment/TIPS.txt
Normal 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
91
assignment/atomicdevs.py
Normal 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
96
assignment/environment.py
Normal 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 ###
|
||||||
56
assignment/plot_template.py
Normal file
56
assignment/plot_template.py
Normal 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
109
assignment/runner.py
Normal 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
61
assignment/system.py
Normal 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 ###
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue