## Introduction

You will use (classic) DEVS to model a queueing and load balancing system for a set of waterway locks. A conceptual view of the system is shown here:

Ships move in the direction of the arrows. A generator generates ships at pseudo-random time intervals, which are added to a queue. Whenever the queue has a ship available, and one of the locks has enough remaining capacity for that ship, the load balancer pulls a ship from the queue and sends it to that lock. A lock may fit more than one ship, so as long as it is not filled up to full capacity, it may wait for more ships to arrive before the lock doors close and the ships can pass through to the other side of the lock. At the end of the system, we have a Sink, where all ships are collected, so we can extract statistics to analyse performance.

Ships can have different sizes. For simplicity, the size of a ship is a small integer (e.g., 1 or 2). Locks can have different capacities: for instance, a lock of capacity 3 will fit either:

- 3 ships of size 1
- 1 ship of size 2 + 1 ship of size 1

## Specification

We now give an overview of the different DEVS components, and their behavior, and their parameters. Although many of the parameters are fixed, your solution must work with different parameters as well. In other words, don't hardcode the parameter values in your DEVS blocks!

### Atomic DEVS blocks

- Generator
  - seed (int): Seed for the random number generator.
  - gen_num (int): The number of ships to generate during a simulation run. This parameter is fixed at 500.
  - gen_rate (float): The average ship generation rate (in ships/second). This parameter is fixed at 1/60/4 = once every 4 minutes.
  - gen_types (list of int): The different ship sizes to generate, and their likelihood. This parameter is fixed at [1,1,2], meaning, we have ship sizes 1 and 2, and size 1 is twice as likely to be generated as 2 (sizes are uniformly sampled from this sequence)
- Queue
  - ship_sizes (set of int): The different ship sizes. We will use {1,2}. The Queue will internally create one FIFO queue per ship size. This makes it possible to give bigger ships a higher priority over smaller ships, or vice versa.
- Load Balancer
  - lock_capacities (list of int): The load balancer needs to know about the number of locks and their capacities. We will use [3,2], meaning two locks, of capacities 3 and 2.
  - priority (enum): Whether to give higher priority to bigger or smaller ships.

  Further, two different load balancer strategies will be implemented. Each strategy will be implemented as a separate Atomic DEVS block:

  - RoundRobinLoadBalancer: will iterate over the locks, and attempt to move a ship into each lock in a round-robin fashion. The priority-parameter is taken into account: if bigger ships have higher priority, then it will first try to move the biggest ship into the lock, then the next-biggest-ship, etc.

    TIP: This load balancer will need to remember (in its DEVS state) the lock into which a ship was moved most recently, so for the next ship, it will start with the lock after it.

    Initially, the RoundRobinLoadBalancer must start with the first lock in the lock_capacities-list.

  - FillErUpLoadBalancer: will prioritize, above all, a "move" that maximally fills up a lock.

    Example: if ships of sizes 1 and 2 are available, and the locks have remaining capacities 3 and 2, then the possible "moves" are:

    - move ship size 1 -> lock with cap 3 => remaining lock cap = 2
    - move ship size 1 -> lock with cap 2 => remaining lock cap = 1
    - move ship size 2 -> lock with cap 3 => remaining lock cap = 1
    - move ship size 2 -> lock with cap 2 => remaining lock cap = 0
    this strategy will prioritize the last move, because it results in the smallest remaining capacity (in this case, completely filling up the lock).

    NOTE: This load balancer also has too take into account the priority-parameter. For instance, if bigger ships are prioritized, and the following moves can be made:

    - move ship size 1 -> lock with cap 2 => remaining lock cap = 1
    - move ship size 1 -> lock with cap 1 => remaining lock cap = 0
    - move ship size 2 -> lock with cap 2 => remaining lock cap = 0
    - move ship size 2 -> lock with cap 1 => remaining lock cap = 1
    this time, both 1->1 and 2->2 completely fill up the lock, but the move 2->2 will be chosen, because it involves a bigger ship.

    Finally, if the same ship can be moved to different locks with equal priority (filling both locks up equally), then the earliest lock in the lock_capacities-list is chosen.

- Lock
  - capacity (int): Capacity of the lock. E.g., 3 or 2.
  - passthrough_duration (float): Time duration (in seconds) of the "passthrough"-procedure. This procedure conceptually involves closing the lock doors, changing the water level, and opening the lock doors on the other side, and the ships leaving the lock. In our simulation, it is only an amount of time during which the lock has zero remaining capacity, after which the ships are sent to the sink. For simplicity, there is no time delay between sending the ships to the sink, and the lock becoming available again (at original capacity).
  - max_wait_duration (float): When a lock is completely filled up (zero remaining capacity), the "passthrough"-procedure starts immediately. The procedure may also start if the lock is non-empty, and a certain amount of time has passed since the first ship has entered the lock. This parameter is that amount of time.

The specification of the semantics of the Atomic DEVS blocks is entirely deterministic. If you implement everything correctly, the system as-a-whole will behave 100% identical to the teacher's solution.

### Coupled DEVS

The system as a whole is modeled as a Coupled DEVS block. Its parameters are mostly passed as-is to the underlying Atomic DEVS blocks. They are:

- gen_num (int) -> Generator.
- gen_rate (float) -> Generator.
- gen_types (list of int) -> Generator and Queue. The Queue only needs this parameter to know the different ship sizes.
- lock_capacities (list of int) -> LoadBalancer and each Lock its respective capacity.
- priority (enum) -> LoadBalancer.
- max_wait_duration (float) -> each Lock (same value for all locks).
- passthrough_duration (float) -> each Lock (same value for all locks).

## What is expected

First of all, you are given an implementation of the following AtomicDEVS blocks, which you must not edit:

- Generator
  - out_ship (Ship): output port on which an event is sent when a ship is generated.
- Sink
  - in_ships (list of Ship): input port on which an event is sent when ships leave a lock. For each ship, the time duration spent in the system is computed and stored.

You will:

- Implement the AtomicDEVS blocks for Queue, RoundRobinLoadBalancer, FillErUpLoadBalancer and Lock.
- Think of the interfaces of these blocks (input/output ports and their events) and the protocol spoken by them.
- Finally, in the CoupledDEVS block representing the entire system, you will have to make the right connections (which of course depends on the input/output ports that you have defined in your AtomicDEVS).

## Goal: Performance Analysis

Once you have implemented the system, we will do performance analysis, comparing combinations of the following parameter values:

- max_wait_duration (float): we will try 0, 2, 4, 6, 8 minutes.
- priority (enum): we will try giving priority to bigger and smaller ships.
- strategy (enum): we will try "round-robin" and "fill-er-up".

More specifically, we would like to know under which (combinations of) parameter values the (avg/min/max) duration that ships spend in the system is minimized. Also, we'd like to know if one choice (e.g., prioritize bigger) always better than another choice (e.g., prioritize smaller), or does it depend on the choices made for the other parameters?

## Getting Started

1. Clone the `mosis24` branch of this git repository.
2. Under the `assignment` directory, you'll find the following files:
   - `runner.py` This script runs the simulation for all combinations of parameter values as described in the Performance Analysis section. It will generate `.csv` files with the time durations that each ship has spent in the system. Every row is a ship (500 ships, so 500 rows total), and every column represents a different value for the max_wait_duration parameter. It will also generate a `plot.gnuplot` file, which you can run with gnuplot as follows:

         gnuplot plot.gnuplot

     which will result in a number of SVG files containing plots of the CSV files.

     You are only allowed to make temporary changes (for debugging) to this file.

   - `system.py` Contains the full system, modeled as CoupledDEVS. You need to edit this file.
   - `atomicdevs.py` Contains skeletons for the AtomicDEVS blocks that you must implement. You need to edit this file.
   - `environment.py` Contains implementations of the Generator, Sink and Ship types. You must not edit this file.
3. Write a report, where you:
   - explain and motivate the interfaces/protocol of the AtomicDEVS blocks
   - show the plotted results
   - interpret the plotted results
4. Submit via BlackBoard, a ZIP file, containing:
   - Your code (only the `assignment` directory)
   - Your report (PDF)
   - The generated CSV- and SVG-files

## Attention!

You must stick to the rules of DEVS:

- The functions `timeAdvance` and `outputFnc` are purely getters! They must not mutate the DEVS state, or any variable, anywhere.
- The functions `extTransition` and `intTransition` may mutate ONLY the DEVS state.

Any violation of these rules results in an incorrect solution. Points will be subtracted.

## Coding Conventions

Please follow these coding conventions:

- Write your AtomicDEVS 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
- Input/output port attributes start with 'in_' and 'out_'

## Troubleshooting

Common mistakes include:

- 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!!)

## Extra Material

- This assignment was inspired by the queueuing example, which you can in `examples/queueing`.