A document from MCS 275 Spring 2024, instructor Emily Dumas. You can also get the notebook file.

MCS 275 Spring 2024 Project 1 - Logistics automation

  • Course Instructor: Emily Dumas

Instructions:

Deadline is 11:59pm central time on Friday 9 February 2024

Note that the hour of the deadline is not the same as for homework assignments.

On the day of the deadline, it may not be possible for the instructor to answer questions received after 3pm.

Collaboration

Collaboration is prohibited. We want to evaluate you on the basis of what you can do by consulting the resources listed below, and without help from anyone other than course staff. Seeking or giving aid on this assignment is prohibited (except for asking course staff); doing so constitutes academic misconduct which can have serious consequences. If you are unsure about whether something is allowed, ask. The course syllabus contains more information about the course and university policies regarding academic honesty.

Topic

In this project you will read some existing object-oriented code and documentation in order to understand how to use it. You will then write some new subclasses that interact with the existing code to perform requested functions.

The classes in this project represent a simplified model of factory automation system that loads finished goods onto trucks.

Resources you are allowed to consult

  • Documents and videos posted to the course web page
  • Any textbook listed on the course blackboard site under "Textbooks"

Ask if you're not sure whether a resource falls into one of these categories, or if you think I missed a resource that seems necessary to complete the assignment.

What to do if you're stuck

Ask your instructor or TA a question by email, in office hours, or on discord.

The pitch: Automated solution to logistics challenge

Shipping parts

You are an employee of a manufacturing company that produces parts for wind turbines. Once manufactured, the parts are packed into crates and shipped by truck to another company that assembles the turbines (using parts from several other manufacturers, too).

There are three types of parts (0, 1, and 2) your company makes, but they share a number of properties:

  • They are all approximately the same size, so one part of any type fits in (and fills) a standard shipping crate
  • It takes two hours to make a single part of any type,
  • Each part has a dedicated production line that manufactures that type of part 24 hours a day.

Since three shipping crates fit in a truck, this makes coordination of shipping easy: When a truck pulls up it waits for one crate to come off of each production line. Then the truck drives away with one part of type 0, one of type 1, and one of type 2.

This entire process has been automated using robotic loaders, and all the plant's equipment is controlled by software written in Python.

A complication appears

When your company's contract to produce turbine parts is renewed, the scope of work expands. Now they must:

  • Produce five types of parts (0,1,2,3,4) by adding two new production lines (for types 3,4)
  • Upgrade the existing production lines so the manufacturing time is less than two hours, though this means the exact time required to produce a part varies from line to line and over the course of the day.

Thankfully, all five parts are still of the same size, so you can still use the standard crates.

These changes create problems in the plant's shipping depot. Trucks will now arrive more frequently, but each truck still only holds three crates. With five lines producing five types of parts at varying and unpredictable rates, some sort of plan is needed to decide which crates to load into a truck when it arrives. Further complicating things is the fact that storage space at the plant is very limited; the depot itself has no extra space, and each production line can only store a few of its finished parts before they run out of room and need to shut the line down.

You have been assigned to solve this problem by updating the software controlling the robotic loaders. The existing Python software uses sensors in the plant to check and report how many crates are waiting on each production line, and is able to instruct the robotic loaders to move the next crate from any line to a truck. A class representing the idea of a truck-loading strategy has already been sketched out, so all you need to do is create subclasses that implement various strategies the plant will test.

The programming details

First, get the starter pack

It is a ZIP archive, i.e. a group of files bundled together and compressed into a single file. The first thing you need to do is to extract the starter pack into a directory where you will do your work. Contact the instructor or TA if you need assistance extracting a ZIP file. Windows, MacOS, and most Linux distributions come with a ZIP extractor either built into the operating system or pre-installed.

If you want to browse the starter pack files on github, you can also do that. But the ZIP is the easiest way to download all of them at once (as you must do):

What's in the starter pack

It contains A FILE YOU EDIT AND SUBMIT AS YOUR PROJECT:

  • routing.py - Contains the Router base class and a couple of subclasses. You will add more subclasses to the end of this file (without changing any of the classes already present).

There is also a module that is used by routing.py, but which you won't edit at all:

  • fixtures.py - Imported by routing.py. Contains classes related to crates, production lines, loadout of a truck, etc.

Finally, the start pack provides a sample program that shows how these classes can be combined into a simulation of the entire system in operation. Running this program is one way to test your work on the project.

  • simulation.py - Simulation program
  • tui.py - A module used by simulation.py

Expect to read a lot for this project!

This project involves adding subclasses to an existing object-oriented program. In the end, it won't amount to a lot of code. Fully reading and understanding this project description and the contents of routing.py will probably account for more than half of the work.

More generally, reading and understanding technical documentation and existing code are important skills that this project aims to develop.

Summary of the factory software structure

Every object and facility involved in the shipping operation (crates containing turbine parts, waiting areas at the end of the production lines, the strategy for deciding what to put on a truck, etc.) is represented by a Python class or object.

Finished parts that have been packed in crates are tracked by the manufacturing facility software, which creates an instance of class Crate (in fixtures.py) for each one. While each Crate object contains a bunch of data about the contents, the details vary from production line to production line, so the shipping software won't actually look into the contents of any Crate object. You'll treat each Crate object as a unit that gets moved around, not examined.

Each production line stores its finished crated parts in a queue (a line where newly made parts are added at one end and parts are taken out to be shipped from the other end). The state of each such storage area is represented by an object of class CrateQueue (in fixtures.py). That class has methods and attributes you can use to examine how full the storage area is, to examine the next crate waiting to be taken out, and to order the loading robots to take the next crate out and put it on a truck. Thus, calling methods of class CrateQueue is the main job of the system that controls truck-packing.*

As described earlier, each CrateQueue has limited size. If it becomes full, the associated production line shuts down temporarily. Naturally, this is something the company wants to avoid.

Since each truck can hold three crates, the intended contents of a truck can be represented by a list of Crate objects (of length at most 3).

Each strategy for deciding how to load the trucks (a routing strategy) is implemented by a subclass of Router (class in routing.py). The constructor of this class expects five CrateQueue objects, corresponding to the five production lines. The Router class has a method called prepare_load() which will be called any time a truck arrives at the facility. This method should examine the queues of the different production lines, remove up to three Crate objects from them, and return those objects in a list. That list will then be used by the robotic loaders to pack the truck and to generate the shipping manifest and other documents that need to go with it.

Thus, here is pseudocode for how the overall software system works:

In [ ]:
# Make queues to track finished crates
Q0 = CrateQueue(size=5) # queue at production line 0
Q1 = CrateQueue(size=5) # queue at production line 1
Q2 = CrateQueue(size=5) # queue at production line 2
Q3 = CrateQueue(size=8) # queue at production line 3
Q4 = CrateQueue(size=8) # queue at production line 4

# Link the queues to the sensors on each production line
# `ProductionLineMonitor` notices finished crates using
# hardware sensors and adds `Crate` objects to a queue.
# (ProductionLineMonitor class doesn't really exist,
# instead we'll simulate creation of crates.)
N0 = ProductionLineMonitor(sensor_id=0,queue=Q0)
N1 = ProductionLineMonitor(sensor_id=1,queue=Q1)
N2 = ProductionLineMonitor(sensor_id=2,queue=Q2)
N3 = ProductionLineMonitor(sensor_id=3,queue=Q3)
N4 = ProductionLineMonitor(sensor_id=4,queue=Q4)

# Suppose ExcellentRouter is the subclass of Router we're trying out
# (you're going to write a class like this)
# Tell it to use the five queues
R = ExcellentRouter( [Q0,Q1,Q2,Q3,Q4] )

# Automated loader control system
# (doesn't really exist, and in practice we'll test
# routing by looking at return values of prepare_load)
Ldr = RoboticLoaderControl()

# Main loop
while True:
    # Pause until a truck arrives
    wait_for_truck()
    # Make sure all recently created crates have been
    # tracked and entered in the proper queue
    for N in [N0,N1,N2,N3,N4]:
        N.sensor_update() 
    # Ask the router to decide which crates to load
    B = R.prepare_load() # returns list of <=3 crate objects
    # Dispatch loader robots to pack the truck
    Ldr.load_crates(B)

One of the key points to notice in the pseudocode above is that a Router subclass instance needs to allow for the possibility that the contents of the input queues will change (perhaps drastically) between one call to prepare_load() and the next.

Existing classes and methods in routing.py

A few classes are provided. You'll add to the hierarchy, so you'll need to understand what methods and attributes exist.

class Router

Superclass: None

Purpose: Base class for all routing strategies. Doesn't actually do anything useful.

Attributes you need to know about:

  • input_queues - A list of the CrateQueue objects to monitor and take Crates from.

Methods you need to know about:

  • __init__(self,input_queues) - Accepts a list of five CrateQueue objects and stores it as an instance attribute named input_queues.
  • prepare_load(self) - Subclasses will redefine this to do something useful. In this class it simply raises an exception immediately.

class OldRouter

Superclass: Router

Purpose: A minimally updated version of the old packing system from the days when there were only three production lines. It ignores the last two CrateQueue objects and simply takes one Crate from each of the first three queues. No longer suitable to the task, but provided for reference.

Attributes redefined or added in this subclass:

  • none

Methods redefined or added in this subclass

  • prepare_load(self) - Get a Crate from each of the first three input queues. (It is possible one or more of those queues might be empty.) The resulting three (or fewer) crates are returned in a list.

MOST IMPORTANT: Specifications of classes you must add to routing.py

class BreadthRouter

Superclass: Router

Purpose: Router that tries to take three crates from different queues every time.

Attributes required to be redefined or added in this subclass:

  • none

Methods redefined or added in this subclass

  • prepare_load(self) - Examines the input queues in order (0 to 4), taking one crate out if the queue is nonempty, and stopping when three crates have been obtained or when every queue has been considered (whichever happens first). Returns the three (or fewer) crates thus obtained as a list, in the order they were removed from the queues. Note that this process never removes two crates from a single queue.

class CyclingDepthRouter

Superclass: Router

Purpose: A router that has the queues take turns being the "active one". A truck is loaded with as many crates as possible from the active queue, then the active status moves to a different queue.

Attributes required to be redefined or added in this subclass:

  • active_queue - An integer, initially 0, indicating which input queue is to be used next.

Methods required to be redefined or added in this subclass

  • __init__(self,input_queues) - In addition to the work of Router.__init__, must initialize the active_queue attribute to 0.
  • prepare_load(self) - Looks at the input queue at index active_queue in attribute input_queues. Takes three crates out, or as many crates as are available, whichever is smaller, and puts them in a list in the order removed. Increases active_queue by 1, wrapping around to 0 if the result is greater than 4. Returns the list.
    • Note that if the active queue is empty, this function will return an empty list (but still increase active_queue by 1).
  • reset(self) - Sets active_queue back to 0, so the router goes back to the state it was in when first created.

class FillPriorityRouter

Superclass: Router

Purpose: A router that tries to avoid full queues by repeatedly taking crates from whichever queue has the least available space. The intention is to avoid the shutdowns that result from a full queue.

Attributes required to be redefined or added in this subclass:

  • none

Methods redefined or added in this subclass

  • prepare_load(self) - Repeats the process outlined below three times, or until it is determined that all input queues are empty (whichever comes first). Then returns the list of crates removed from queues in that process, in the order removed.
    • Examine the input queues and determine which ones are nonempty right now.
    • Of the nonempty queues, determine the one which has the least remaining space (measured in number of crates that can be added before it becomes full). If there is a tie, prefer the one with the smallest index in input_queues.
    • Take a crate out of the queue selected in the previous step.

class BalanceRouter

Superclass: Router

Purpose: A router that assumes it is desirable to ship the same number of parts from each production line, on average, and which therefore prefers to take parts from a line that's seen the least shipping thus far.

Attributes required to be redefined or added in this subclass:

  • none (i.e. you may need to add attributes, but there are no specific requirements on the names, types, or purposes)

Methods redefined or added in this subclass

  • __init__(self) - In addition to superclass initialization, will need to create an one or more instance attribute(s) that will be used to track the shipping history (i.e. how many crates we've taken out of each production line / queue). Initially, that history simply records that no crates have been taken from any queue.

  • prepare_load(self) - Repeats the process outlined below three times, or until it is determined that all input queues are empty (whichever comes first). Then returns the list of crates removed from queues in that process, in the order removed.

    • Examine the input queues and determine which ones are nonempty right now.
    • Examine the shipping history, and determine which nonempty queue has had the fewest crates taken out of it thus far (the "least shipped available line"). If there is a tie, prefer the one with the smallest index in input_queues.
    • Take a crate out of the least shipped available line and update the shipping history accordingly.
  • reset(self) - Reset the history of shipped crates to the state it was in when the object was initialized (i.e. forget about all the crates shipped thus far).

Hints about the routing strategies

  • CyclingDepthRouter and BalanceRouter are the only "stateful" routers, meaning their behavior depends on their past actions. The other strategies do something that only depends on what's waiting in the queues.

  • Both FillPriorityRouter and BalanceRouter describe how to choose which queue the next crate should be taken from, and they say that process should be repeated three times to fill a truck. Keep in mind that after the first crate is selected and removed, certain things change (e.g. the number waiting in that queue, the number shipped, etc.) and this may influence which queue is chosen the second time, and so on.

  • I think the routing strategies are listed in the previous section in order of increasing difficulty.

Classes in fixtures.py

Now that you know the stragies you need to implement, you'll need to understand how to work with CrateQueue objects to accomplish those strategies.

Note:

  • The Router subclasses you write must tolerate being given an instance of a subclass of CrateQueue as a queue, or an instance of a subclass of Crate as a crate. I recommend you don't do any explicit type checking of arguments.

class CrateQueue

Attributes you may access:

  • maxlen - an integer representing the maximum number of crates that this queue can contain

Methods you may call:

  • peek(self) - Take a look at the next Crate without actually removing. Returns that Crate object. If the queue is empty, raises IndexError.
  • pop(self) - Remove and return the next Crate from the queue. If the queue is empty, raises IndexError.
  • __len__(self) - Return the number of Crate objects currently in this queue. (Automatically called when you apply the built-in len function to a CrateQueue object).
  • __bool__(self) - Returns True if the queue has at least one Crate in it, and False otherwise. (Thus if Q is a CrateQueue, the conditional if Q: means "if Q is not empty:")
  • is_nonempty(self) - same as __bool__.
  • is_empty(self) - Opposite of is_nonempty. Returns True if the queue is empty.
  • remaining_space(self) - Returns the number of Crate objects that can be added before the queue becomes full (an integer).

class Crate

This class is completely opaque. It exists, and is the type of object you get from CrateQueue.peek() and CrateQueue.pop(), but you shouldn't call any methods of class Crate, access any attributes of it, or attempt to instantiate any Crate objects directly. The class Crate might be changed arbitrarily in the version of fixtures.py used to test your submission.

Demonstration program

It is difficult to write a Router subclass if you can't see it working.

The program simulation.py included in the starter pack has the ability to instantiate a Router or subclass thereof and to run a simple simulation. Try it out to see OldRouter in action and then to test your subclasses of Router during development.

IMPORTANT RULES YOUR CODE MUST FOLLOW

Your project must follow the rules in the MCS 275 coding standards document. In addition:

  • Only submit routing.py - The autograder will supply fixtures.py when testing your project. The other files provided in the starter pack are for your testing, and are not used in grading.

  • No changes to existing classes - You are adding subclasses to routing.py, and must not edit the classes already present there.

  • No output - Make sure the methods in routing.py do not print anything and do not write to any files.

  • Only class definitions - The only things you add to routing.py should be class definitions. Do not add any code outside of class definitions, whether active or commeted out (e.g. no test code, no commented-out false starts, etc.). In particular, importing routing.py shouldn't result in anything happening in the terminal.

  • Make proper use of inheritance - when a superclass contains something useful to a subclass, use it directly (through inheritance, method calls, or attribute accesses).

  • No floats - Do not use any values of type float. The only numeric type you need in this project is int (integer).

How your project will be graded

Autograder: 40 points

The autograder tests your program and grades it based on its behavior. A series of tests will be run including simulations under various circumstances. These tests will look for adherence to the specifications in this document.

Manual review: 10 points

I will review your code and look for adherence to the coding standards and other rules given above, and I will examine your method of solving the problem. If I see that your program does not do what was requested in this document, but the error was not detected in the automated testing, a deduction may be given at this point. The scores assigned by the autograder will not be changed during manual review unless I discover some kind of intentional wrongdoing (such as an attempt to circumvent or reverse-engineer the autograder's operation).

Revision history

  • 2024-01-28 Initial publication
  • 2024-01-29 Fix an incorrect class name in the description of CyclingDepthRouter description (2x)
  • 2024-02-06 Fixed deadline so it agrees with the syllabus; this change makes the deadline later