Browse Source

add starting point for mosis 2024 assignment

Joeri Exelmans 10 months ago
parent
commit
4b959bc98b

+ 2 - 0
.gitignore

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

+ 1 - 0
assignment/.gitignore

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

+ 24 - 0
assignment/TIPS.txt

@@ -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 - 0
assignment/atomicdevs.py

@@ -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 - 0
assignment/environment.py

@@ -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 - 0
assignment/plot_template.py

@@ -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 - 0
assignment/runner.py

@@ -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 - 0
assignment/system.py

@@ -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 ###

+ 1 - 0
examples/queueing/experiment.py

@@ -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