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

MCS 275 Spring 2023 Project 1 - Marble Factory

  • Course instructor: David Dumas

Instructions

Deadline is 6pm central on Friday 10 February 2023.

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

Collaboration policy and academic honesty

This project must be completed individually. 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 in order to understand how to use it. You will then write some new subclasses that interact with the existing code.

The classes in this project represent a simplified version of a system for automatically shuttling raw materials between a supply depot and a manufacturing plant.

Resources you are allowed to consult

  • Documents and videos posted to the course web page
  • Any of the optional textbooks listed on the course web page

Ask if you are unsure whether a resource falls under one of these categories, or if you think I missed an essential resource.

What to do if you are stuck

Contact the instructor or your TA by email, in office hours, or on discord.

The pitch: Autopilot monorail at the marble factory

This is a fictional scenario that explains the work you need to do for this project in the context of a real-world problem

Making marbles

You are contracted to work on a software development project for a factory that produces marbles---small glass spheres that are used for various games.

marbles.jpg
Marbles. Photo by: James Petts

The factory makes marbles by mixing sand with trace amounts of other substances (which affect the color and melting point of the mixture), melting it in a furnace, and forming the molten mixture into spheres. Large quantities of sand are needed, so keeping the factory supplied with enough sand is an important concern.

sand.jpg
A pile of sand. Photo by: P K

This factory is located in building 275 of an industrial park, which is unfortunately some distance from the industrial park's supply depot where trucks can bring sand to supply the factory. Thus, arriving trucks dump sand into a supply cache (a large pile) located at the depot. Then the sand needs to be transported to the factory by a smaller monorail car (the shuttle) that runs on a track from the depot to building 275. Next to building 275, another large pile (the destination cache) holds the sand that has arrived at the factory and is waiting to be used. This is shown in the diagram below:

park-diagram.png
Poor cartoon rendering of the monorail track from supply depot to factory 275

Programming the monorail

The marble factory plans to automate sand transportation by the shuttle as much as possible. Engineers at the factory have already developed systems to monitor the supply cache and destination cache, and to load or unload the shuttle at these locations. What they need now is a software system to pilot the shuttle. They've decided that this will be written in Python, and developed as a single class that will run the car in a simple loop of this general form:

In [ ]:
sc = fixtures.SupplyCache() # class that monitors the supply cache (not your responsibility)
dc = fixtures.DestinationCache() # class that monitors the destination cache (not your responsibility)

# Make the shuttle autopilot (YOUR RESPONSIBILITY), passing the objects that
# represent the supply and destination caches so it can interact with them
S = shuttle.Shuttle(supply=sc, dest=dc)
# `Shuttle` is a base class that doesn't do any piloting; you will write
# subclasses that actually do things.

# ...
# other intialization here
# ...
while True:
    # Do anything needed to bring SupplyCache and DestinationCache info up to date
    # ...
    
    # Ask the shuttle to decide on and execute its next operation
    S.update() 
    
    # Wait until it's time for the next update
    # ...

The factory engineers have already decided on the methods that the SupplyCache and DestinationCache contain, and as mentioned above, those components have already been written. They have also developed a base class called Shuttle that every piloting strategy will be a subclass of, and a list of the strategies that they want implemented as subclasses.

You have been hired to complete the development work by actually writing those subclasses of Shuttle that interact with the other components and complete the system. The rest of this document details what that requires.

Coordinates for the monorail track

As seen in the diagram above, the monorail track leading from the supply cache to the destination cache has been given coordinates where the supply cache corresponds to x=0, the destination cache to x=9, and the track to the interval between 0 and 9. It has been decided that the shuttle movement will always be specified using positions with integer coordinates, which works well because in these coordinates the shuttle can move one unit per minute.

Things the monorail can do

The program controlling the shuttle lives in a single method update() of a subclass of Shuttle (which is in the module shuttle, so most code will actually refer to it by shuttle.Shuttle). This method is called once per minute, and must decide what to do over the course of the following minute. The following actions are allowed:

  • Move one step in the positive direction (increasing its position value by 1), if it's not already at end of the track (position 9).
  • Move one step in the negative direction (decreasing its position value by 1), if it's not already at the start of the track (position 0).
  • If the position is 0 (supply cache), request to load a certain amount of sand from the supply cache. (The actual amount loaded will depend on how much is available, and how much room is left in the shuttle.)
  • If the position is 9 (destination cache), request to unload a certain amount of sand. (The actual amount unloaded will depend on how much room is left in the destination cache, and how much sand is on the shuttle.)

Note that the shuttle can only do ONE of these things in any one minute period, and must decide what to do during the call to update().

As you can see, deciding what operations to perform is likely to depend on an ability to check various aspects of the environment (e.g. amount of sand at each cache, capacity of each cache, monorail shuttle capacity, etc.). There are methods for this purpose, documented below.

The programming details

First, get the starter pack

The starter pack is available here:

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. All common operating systems come with a ZIP extractor built in.

If you want to browse the files on github, you can 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:

  • shuttle.py - Contains Shuttle class; you need to add three more classes as described below.

And also a supporting file that I provide, and which you don't edit at all:

  • fixtures.py - A module containing classes representing the supply cache and destination cache.

Finally, there is a sample program that shows how these classes can be combined into a simulation of the entire system in operation, which you can edit and use to test your work.

  • simulation.py - Simulation program
  • tui.py - 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 such cases, just understanding the current state of the program is usually a significant part of the work. More generally, reading and understanding both technical documentation and existing code is an important skill.

Documentation of classes provided in the starter pack

This section contains technical documentation for classes in the starter pack you need to know about. They provide the features that an autopilot needs to do its work.

Classes in fixtures.py

DO NOT EDIT THIS FILE AT ALL. Reading it is a good idea, but you must use it in its unmodified form if you want your project to pass the automated tests. The code you add to shuttle.py will make use of the classes defined in this file, which are documented below.

class SupplyCache

Superclass: Cache (not documented here)

Purpose: Base class of all classes that simulate the supply cache at position 0, which is a pile of sand at the supply depot.

Methods:

  • __init__(self,capacity) : Initialize a supply cache with the given capacity (an integer).
  • update(self) : Simulate the passage of one minute. In this class, it does nothing. Subclasses used for testing may do things (like add more sand on occasion, to simulate arriving trucks).
  • remove_material(self,amount) : Request to remove amount kg of sand from the supply cache. Returns the actual amount removed, which might be smaller. For example, if the supply only has 150kg at a time when you ask to remove 300kg, this function would remove all 150kg and return that number. It is the caller's responsibility to not ask for more sand than they can handle.
    • Note: the caller of this function is responsible for recording where the sand went (e.g. adding it to the amount held by the monorail).
    • A subclass of Shuttle is expected to remove material from the supply by calling this method.==
  • is_full(self) : Return a boolean indicating whether the supply is full (amount stored is equal to capacity)
  • is_empty(self) : Return a boolean indicating whether the supply is empty (amount stored is zero)

class DestinationCache

Superclass: Cache (not documented here)

Purpose: Base class of all classes that simulate the destination cache at position 9, a pile of sand at the entry point of the factory.

Methods:

  • __init__(self,capacity) : Initialize a supply cache with the given capacity (an integer).
  • update(self) : Simulate the passage of one minute. In this class, it does nothing. Subclasses used for testing may do things (like remove some sand to account for usage by the factory).
  • add_material(self,amount) : Request to add amount kg of sand to the destination cache. Returns the actual amount added, which might be smaller. For example, if the destination cache only has room for 150kg more at a time when you ask to add 200kg, this function would add 150kg and return that number. It is the caller's responsibility to not ask to add more sand than they actually have on the shuttle; this function assumes that amount kg of sand are available.
    • Note: the caller of this function is responsible for recording the removal of that sand from wherever it came from (e.g. subtracting it from the amount held by the monorail).
    • A subclass of Shuttle is expected to deposit material at the destination by calling this method.
  • is_full(self) : Return a boolean indicating whether the destination cache is full (amount stored is equal to capacity)
  • is_empty(self) : Return a boolean indicating whether the destination cache is empty (amount stored is zero)

Guarantees about subclasses of SupplyCache and DestinationCache

When testing your project, we'll use subclasses of SupplyCache and DestinationCache that simulate additional sand arriving at the supply cache, factory consumption of sand from the destination cache, or a combination thereof.

However, you are guaranteed that:

  • In any subclass of SupplyCache, the only way the amount of available sand can decrease is if you call remove_material(). However, the amount of sand available may increase between calls to Shuttle.update() to account for new materials arriving (e.g. a truck of sand arrives and is added to the supply).

  • In any subclass of DestinationCache, the only way the amount of available sand can increase is if you call add_material(). However, the amount of sand available may decrease between calls to Shuttle.update() to account for usage of sand by the factory.

Classes in shuttle.py

DO NOT EDIT THE CLASS LISTED BELOW. You will add subclasses to this file, but the existing class must not be changed.

class Shuttle

Superclass: None

Purpose: The base class from which all monorail shuttle autopilot systems inherit. It sits at position 0 forever doing nothing.

Attributes: All of these are created/set by the constructor:

  • supply : The supply cache object given to the constructor (an instance of a subclass of SupplyCache)
  • dest : The destination cache object given to the constructor (an instance of a subclass of DestinationCache)
  • capacity : Storage capacity in kg (never changes; set to capacity argument of constructor)
  • stored : Amount of sand currently on board, in kg (initially 0)
  • pos : Current position (an integer between 0 and 9, inclusive, initially 0)

Methods:

  • __init__(self,capacity,supply,dest) : Create a new instance with the given capacity (in kg), which interacts with the given SupplyCache instance supply located at position 0 and the given DestinationCache instance dest at position 9. Sets the initial position and the initial stored amount to 0.
  • available_capacity(self) : Returns the amount of additional sand (in kg) the shuttle could take on at this moment.
  • update(self) : A method to simulate one minute of time passing; does nothing in this class, and is expected to be overridden in subclasses you write.

THE MOST IMPORTANT SECTION: Classes you must write

Everything you write goes in shuttle.py.

class BackForthShuttle

Superclass: Shuttle

Purpose: Represents a shuttle autopilot strategy that goes back and forth between supply and destination caches. In this class it does so with minimal pauses at each end to load or unload. Subclasses could modify that behavior. It doesn't care whether it has any room for sand, or whether the supply or destination is full or empty. It does the best it can on a fixed schedule of movement.

Methods to be defined in this subclass (i.e. not just inherited):

  • __init__(self,...) - Same args as the superclass constructor, but you might want to override this and add additional code to set up things like the state. (See update() docs below.)
  • update(self) - Account for one minute of simulated shuttle piloting. This class uses the following strategy:
    • Overall, the plan could be summarized as: Load, move forward, unload, move backward, repeat
    • Load: If the object is new, or if it has just arrived at position 0 in the previous update() call, remove as much material as possible from the supply cache as possible.
    • Move forward: If the previous update() call tried to remove material from the supply cache, begin moving toward the destination cache, moving one unit per minute until we arrive (which spans many subsequent update() calls)
    • Unload: If the previous update() call arrived at the destination cache, unload as much material as possible.
    • Move backward: If the previous update() call tried to add material to the desintation cache, begin moving back toward the supply cache, moving one unit per minute until we arrive (which spans many subsequent update() calls)

Hint:

  • A good design would add an attribute state that tracks what the shuttle is currently doing, and have update check this, decide what to do, and possibly change the value of state accordingly.

Add or override other methods or attributes at your discretion.

class ActOnOverflowShuttle

Superclass: You choose, but it must descend from Shuttle. You might want to make it a subclass of BackForthShuttle.

Purpose: Represents a shuttle autopilot strategy waits at the supply until the supply becomes full, then it takes as much sand as possible to the destination and comes back.

Methods to be defined in this subclass (i.e. not just inherited):

  • __init__(self,...) - Same args as the superclass constructor, but you might want to override this and add additional code to set up things like the state. (See update() docs below.)
  • update(self) - Account for one minute of simulated shuttle piloting. This class uses the following strategy:
    • Overall, the plan could be summarized as: Wait, move forward, unload, move backward, repeat
    • Wait: If the object is new, or if it has just arrived at position 0 in the previous update() call, check to see if the supply is full (an "overflow"). If it is, remove as much material as possible from the supply cache. If the supply isn't full, do nothing.
    • Move forward: If the previous update() call tried to remove material from the supply cache, begin moving toward the destination cache, moving one unit per minute until we arrive (which spans many subsequent update() calls)
    • Unload: If the previous update() call arrived at the destination cache, unload as much material as possible.
    • Move backward: If the previous update() call tried to add material to the desintation cache, begin moving back toward the supply cache, moving one unit per minute until we arrive (which spans many subsequent update() calls)

Add or override other methods or attributes at your discretion.

Hint:

  • Notice that this class will make a trip from supply to destination any time it detects the supply is full while sitting at the supply. Even if the shuttle is already full of sand, or if the destination cache is full, it still makes a trip. That means a single trip won't necessarily reduce the amount at the supply or increase the amount at the destination cache.

class ActOnUnderflowShuttle

Superclass: You choose, but it must descend from Shuttle. You might want to make it a subclass of BackForthShuttle.

Purpose: Represents a shuttle autopilot strategy waits at the supply until the destination cache is empty and the supply contains some sand. It then loads sand, takes it to the destination, and returns.

Methods to be defined in this subclass (i.e. not just inherited):

  • __init__(self,...) - Same args as the superclass constructor, but you might want to override this and add additional code to set up things like the state. (See update() docs below.)

  • update(self) - Account for one minute of simulated shuttle piloting. This class uses the following strategy:

    • Overall, the plan could be summarized as: Wait, move forward, unload, move backward, repeat

    • Wait: If the object is new, or if it has just arrived at position 0 in the previous update() call, check to see if these conditions are satisfied:

      1. The destination cache is empty (an "underflow"), and
      2. The supply cache contains at least 1kg of material

      If these conditions are satisfied, remove as much material as possible from the supply cache (i.e. request such removal). Otherwise, do nothing (i.e. wait for the next update() call).

    • Move forward: If the previous update() call requested to removed material from the supply cache, begin moving toward the destination cache, moving one unit per minute until we arrive (which spans many subsequent update() calls)

    • Unload: If the previous update() call arrived at the destination cache, unload as much material as possible.

    • Move backward: If the previous update() call tried to add material to the desintation cache, begin moving back toward the supply cache, moving one unit per minute until we arrive (which spans many subsequent update() calls)

Add or override other methods or attributes at your discretion.

Hint:

  • Notice that this class is a bit "smarter" than ActOnOverflowShuttle, in that it will notice and avoid a situation in which a trip to the destination would be useless: It will not attempt to remove material from an empty supply, instead waiting around until some sand was available.

How do I do this stuff?

As explained earlier, the key is to make subclasses of Shuttle that include their own update() methods to do the actual work of deciding the next step.

In an earlier section we listed the things the shuttle can do in a single update() call, and so a key part of this assignment is to determine how those actions would be accomplished by

  • Modifying attributes of the Shuttle subclass instance, or
  • Calling methods of an object that is available to the Shuttle subclass instance (such as supply and dest).

Then, you need to write a function that can decide which action is needed at any given time.

These autopilot strategies are stateful (they maintain some internal state that determines what they're currently doing, and the behavior depends on that state). You will probably want to add an attribute to the Shuttle subclass in order to track that state information. Looking back at the strategy we used to write the PatrolBot class in Lecture 6 is probably a good starting point.

Demonstration program

It is difficult to write an autopilot class if you can't see it working.

The program simulation.py included in the starter pack has the ability to instantiate a SupplyCache, a DestinationCache, a Shuttle subclass, and to run a simulation. The simulation uses text graphics, and expects you to press Enter to advance by one minute.

As provided, this program just makes a Shuttle instance, which sits at the supply forever doing nothing. To test the behavior of the classes you write, edit this program and have it create an instance of BackForthShuttle, ActOnOverflowShuttle, or ActOnUnderflowShuttle instead. The program also has comments showing where you'd put code to add new material to the supply, remove. The autograder will test your shuttle autopilot classes with SupplyCache and DestinationCache subclasses that simulate new sand arriving and sand being used by the factory (respectively), so activating these features of simulation.py is a good idea.

Example simulation

Here's an example simulation where:

  • The autopilot strategy is ActOnUnderflowShuttle, with a capacity of 200kg.
  • The SupplyCache has a capacity of 900kg, starts full, and is never added to.
  • The DestinationCache has a capacity of 500kg, starts empty, and has 10kg removed per minute to account for factory usage.

This simulation is not exactly the same as the provided simulation.py; the program shown here also prints a description of what happened in each minute.

The simulation proceeds for 141 steps before the supply is empty, after which there is nothing more for the shuttle to do.

It's a video of the terminal. Play to watch the simulation.

Contact the instructor if this video doesn't work for you!

IMPORTANT RULES YOUR CODE MUST FOLLOW

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

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

  • The only things you add to shuttle.py should be class definitions. Do not add any code outside of class definitions (no functions that are not methods, no test code, etc.)

  • Make use of inheritance when you can, and when it helps make the code simpler (e.g. avoiding duplication).

  • Do not use float values in any way. 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 one-step and multi-step 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).

Final word: It is really important to test locally

As you write your project, test it locally on your own computer or the lab computer you use for your work. Every time you create a new Shuttle subclass, add an instance of it to simulation.py and try it out. It is much harder to debug a broken program based solely on reports you get from the autograder compared to working with your local Python interpreter.

Revision history

  • 2023-01-26 Initial publication