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*
|
||||
test/output/*
|
||||
/.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
|
||||
sim = Simulator(m)
|
||||
sim.setClassicDEVS()
|
||||
# sim.setVerbose() # <- uncomment to see what's going on
|
||||
sim.simulate()
|
||||
|
||||
# Gather information for output
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue