A document from MCS 260 Fall 2021, instructor Emily Dumas.
You can also get the notebook file.

- Course instructor: Emily Dumas

- Oct 18: Last lecture whose material is needed for this project (Lecture 24)
- Oct 22: Project description released
- Nov 1: Autograder opens
- Nov 3: Date by which you should make your first autograder submission (if you're following the schedule I suggest)
- Nov 5, 11:00-11:50am: My last office hours before the deadline
- Nov 5, 1:30pm: Latest time you can count on reaching me by email
- Nov 5, 6:00pm: Deadline for final submission

This project must be completed **individually**. Seeking or giving aid on this assignment is prohibited; doing so constitutes academic misconduct which can have serious consequences. The only resources you are allowed to consult are the ones listed below. 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.

- Documents and videos posted to the course web page
- Any of the optional textbooks listed on the course web page
- Instructor or TA

**Ask** if you are unsure whether a resource falls under one of these categories.

The rest of this document describes a module `dynam`

that you need to write. It should be contained in a single file called `dynam.py`

. That's the only thing you need to submit to gradescope.

This project is about taking a function of one variable, say $f(x)$, and applying it repeatedly to some starting value $x_0$. When you do that, you get an infinite sequence of values
$$x_0, f(x_0), f(f(x_0)), f(f(f(x_0))), ...$$
which is called the **orbit of $x_0$ under $f$**.

For this project, the functions we'll consider are ones that take an integer as input and produce an integer as output. So, for example, if $f(x) = x^2$ then the orbit of $3$ is the sequence

$$ 3, 9, 81, 6561, 43046721, 1853020188851841, 3433683820292512484657849089281, ... $$These numbers keep getting bigger, never repeating. But here's another example. Consider the function $mwdp(x)$ that was defined in Project 1. If you apply that function repeatedly to the starting value $x_0 = 50$, then you get this sequence (which is the orbit of $50$ under $mwdp$):

$$ 50, 37, 43, 29, 55, 40, 28, 46, 41, 28, 46, 41, 28, 46, 41, 28, 46, 41, 28, 46, 41, ... $$Notice that this sequence wanders around for a while but eventually settles into a pattern where $46, 41, 28$ just repeats forever. We could therefore split the orbit up into two parts:

- The
**initial part**, before anything repeats: $ 50, 37, 43, 29, 55, 40 $ - The
**periodic cycle**, a finite list of values that repeat forever, and which begins immediately after the initial part: $ 28, 46, 41 $

Let's consider one more example. Define a function $$ g(x) = \begin{cases} x/2 & \text{if $x$ is even} \\ 3x+1 & \text{ if $x$ is odd} \end{cases} $$ Then the orbit of $17$ under $g$ looks like this:

$$ 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1, 4, 2, 1, 4, 2, 1, 4, 2, ... $$In this case, the initial part is $17, 52, 26, 13, 40, 20, 10, 5, 16, 8$ followed by the periodic cycle $4,2,1$.

It's possible for a cycle to have just one element, as well. That's called a **fixed point**. For example, if the orbit under some function was

then we'd say the orbit has initial part $5, 11, 29$ and then a periodic cycle of length one, $25$.

**This project is not about iterating any particular function, like $f(x) = x^2$ or $mwdp(x)$ or $g(x)$.** Instead, it's about writing a module of functions that you can use to study the iteration of whatever function you are interested in. For example, the module `dynam`

you need to write is going to contain a function `orbit(f,x0,n)`

that computes the first `n`

terms of the orbit of `x0`

under function `f`

. It could be used like this to study the orbits of the squaring function:

In [3]:

```
import dynam
def f(x):
"""square x"""
return x*x
# First 8 terms in the orbit of 3 under f
print(dynam.orbit(f,3,8))
```

Or, the very same function `orbit`

from module `dynam`

could be used to study orbits of the `mwdp`

function like this:

In [5]:

```
import dynam
def mwdp(x):
"""mean with digit power"""
digits = [int(c) for c in str(x)]
maxdigit = max(digits)
numdigits = len(digits)
return (x + maxdigit ** numdigits) // 2
# First 30 terms in the orbit of 50 under mwdp
print(dynam.orbit(mwdp,50,30))
```

So `dynam.orbit`

is an example of a *higher-order function*: One of its arguments is a function, and it uses that function to do its work. A program using `dynam.orbit`

can specify any function it likes.

Below you'll find the **skeleton** for the module `dynam`

. It contains function definitions and docstrings, but the functions lack bodies. You are expected to fill in the bodies of the functions so that they work exactly as the docstrings say they do.

Two orbits of a function might end up in the same repeating cycle but enter the cycle at different places. For example, considering the function $mwdp(x)$, the orbit of $39$ is:

$$39, 60, 48, 56, \mathbf{46}, 41, 28, 46, 41, 28, 46, 41, 28, 46, 41, 28, ...$$while we saw earlier that the orbit of $50$ is:

$$50, 37, 43, 29, 55, 40, \mathbf{28}, 46, 41, 28, 46, 41, 28, 46, 41, 28, ...$$These are obviously very similar, because they enter the same repeating pattern. But the orbit of $39$ first hits the cycle at the number $46$, while the orbit of $50$ first hits the cycle at $28$. (In both cases the first number from the periodic cycle that appears in the orbit is shown in bold.)

The reason you need to care about this is that the skeleton for the module `dynam`

contains some functions that deal with splitting an orbit into an initial part and repeating cycle; those functions need to notice the place where the repeating pattern begins. However, it also contains some functions that search many starting points to see what cycles are seen. Those functions would not consider the cycle `[46,41,28]`

to be different from `[28,46,41]`

, because they represent the same repeating pattern (just starting at a different spot).

`dynam`

¶Copy the code below into your `dynam.py`

file. Don't change the function names or docstrings, but fill in the bodies of the functions so they do what the docstrings explain.

Don't add any new functions ot the module, either. You are encouraged to write some test code that checks whether the module's functions work, and that test code will need to define some functions (to have something to pass as `f`

), but you should keep those tests separate from the module itself.

In [7]:

```
def orbit(f,x0,n):
"""
Compute the first `n` terms of the orbit of `x0` under
the function `f`.
Arguments:
`f` - a function (should take one integer argument and return an integer)
`x0` - a value of the type that `f` accepts as its argument
`n` - the number of points to compute (a positive integer)
Returns:
A list of length `n` containing the values [ x0, f(x0), f(f(x0)), f(f(f(x0))), ... ]
"""
# PUT THE BODY OF orbit HERE AND DELETE THIS LINE
def orbit_data(f,x0):
"""
Repeatedly apply function `f` to initial value `x0` until some
value is seen twice. Return dictionary of data about the observed
behavior.
Arguments:
`f` - a function (should take one integer argument and return an integer)
`x0` - a value of the type that `f` accepts as its argument
Returns:
Dictionary with the following keys (after each key is a description of
the associated value):
"initial": The part of the orbit up to, but not including, the first
value that is ever repeated.
"cycle": The part of the orbit between the first and second instances of
the first value that appears twice, including the first but not the
second. In other words, the entire orbit consits of the "initial"
part followed by the "cycle" repeating over and over again.
Example: Suppose that applying f repeatedly to start value 11 gives this sequence:
11, 31, 12, 5, 6, 2, 8, 19, 17, 8, 19, 17, 8, 19, 17, 8, ...
Then the return value would be:
{
"initial":[11, 31, 12, 5, 6, 2],
"cycle": [8,19,17]
}
(If the orbit of `x0` doesn't end up in a cycle, it's ok for this function to run forever
trying to find one.)
"""
# PUT THE BODY OF orbit_data HERE AND DELETE THIS LINE
def eventual_period(f,x0):
"""
Determine the length of the periodic cycle that `x0` ends up in.
Arguments:
`f` - a function (should take one integer argument and return an integer)
`x0` - a value of the type that `f` accepts as its argument
Returns:
The length of the periodic cycle that the orbit of `x0` ends up in.
Example: Suppose that applying f repeatedly to start value 11 gives this sequence:
11, 31, 12, 5, 6, 2, 8, 19, 17, 8, 19, 17, 8, 19, 17, 8, ...
Then the return value of eventual_period(f,11) would be 3, since the periodic
cycle contains the 3 values 8,19,17.
(If the orbit of `x0` doesn't end up in a cycle, it's ok for this function to run forever
trying to find one.)
"""
# PUT THE BODY OF eventual_period HERE AND DELETE THIS LINE
def steps_to_enter_cycle(f,x0):
"""
Determine the length of the intial part of the orbit of `x0` under `f` before
it enters a periodic cycle.
Arguments:
`f` - a function (should take one integer argument and return an integer)
`x0` - a value of the type that `f` accepts as its argument
Returns:
The number of elements of the orbit of `f` before the first value that
repeats.
Example: Suppose that applying f repeatedly to start value 11 gives this sequence:
11, 31, 12, 5, 6, 2, 8, 19, 17, 8, 19, 17, 8, 19, 17, 8, ...
Then the return value of steps_to_enter_cycle(f,11) would be 6, because there are 6
values in the intial segment of the orbit (i.e. 11, 31, 12, 5, 6, 2) which are followed by
a periodic cycle.
(If the orbit of `x0` doesn't end up in a cycle, it's ok for this function to run forever
trying to find one.)
"""
# PUT THE BODY OF steps_to_enter_cycle HERE AND DELETE THIS LINE
def eventual_cycle(f,x0):
"""
Return the periodic cycle that the orbit of x0 ends up in as a list.
Arguments:
`f` - a function (should take one integer argument and return an integer)
`x0` - a value of the type that `f` accepts as its argument
Returns:
The earliest segment from the orbit of `x0` under `f` that repeats
indefinitely thereafter, as a list.
Example: Suppose that applying f repeatedly to start value 11 gives this sequence:
11, 31, 12, 5, 6, 2, 8, 19, 17, 8, 19, 17, 8, 19, 17, 8, ...
Then eventual_cycle(f,x0) would return [8, 19, 17].
(If the orbit of `x0` doesn't end up in a cycle, it's ok for this function to run forever
trying to find one.)
"""
# PUT THE BODY OF eventual_cycle HERE AND DELETE THIS LINE
def smallest_first(L):
"""
Rotates a list so that its smallest element appears first.
Arguments:
`L`: A list of integers, no two of them equal
Returns:
A list that is the result of moving the first element of `L` to the end,
repeatedly, until the first element of `L` is the smallest element of the list.
Example: smallest_first([46,41,28]) returns [28,46,41]
Example: smallest_first([4,2,1]) returns [1,4,2]
Example: smallest_first([9,8,7,6,5,4,3,2,1]) returns [1,9,8,7,6,5,4,3,2]
"""
# PUT THE BODY OF smallest_first HERE AND DELETE THIS LINE
def find_cycles(f,start_vals):
"""
Find all the periodic cycles of the function `f` that appear when you consider
orbits of the elements of `start_vals`.
Arguments:
`f` - a function (should take one integer argument and return an integer)
`start_vals` - a list of integers to use as starting values
Returns:
A list of lists, consisting of all the periodic cycles that are seen
in the orbits of the start values from `start_vals`. Each cycle is
given with its smallest entry appearing first, and any given cycle
appears only once in the list.
e.g. If `mwdp` is the mean with digit power function, then find_cycles(mwdp,[65,66,67])
would return [ [28,46,41], [38,51] ] because both 65 and 67 end up in the [28,46,41]
cycle and 66 ends up in the [38,51] cycle.
"""
# PUT THE BODY OF find_cycles HERE AND DELETE THIS LINE
```

I suggest you write the functions in the order shown in the skeleton, i.e. work from top to bottom.

The function `orbit_data`

is probably the hardest to write, with `find_cycles`

second hardest.

You can, and should, have some of the functions in your module call other functions! Specifically:

I suggest you use the function

`orbit_data`

inside these functions:`eventual_period`

`steps_to_enter_cycle`

`eventual_cycle`

I suggest you use both

`eventual_cycle`

and`smallest_first`

inside`find_cycles`

The functions in your module `dynam`

probably won't work perfectly the first time. Testing in the REPL is possible, but tedious. You'd need to open the Python interpreter, import the module, define a function of an integer argument, and then apply one of the functions from `dynam`

to the function you defined.

Using the autograder as your only way of testing has its own problems: It isn't available until Nov 1, it's slow, and if your program has unexpected types of errors (like syntax problems), it can be hard to understand where the problem is based on the autograder's report.

For all these reasons, you should test your work locally, on whatever computer you use to write code. Submit to the autograder when things appear to be working locally.

For local testing, I recommend you make a script called `test_dynam.py`

that imports `dynam`

, defines some functions of an integer argument to use with it, and then calls various functions from `dynam`

. It should probably print the results. That way, each time you finish a function in `dynam`

, you can save it, run `python3 test_dynam.py`

in the terminal, and see if the results match your expectations.

The first three lines of your Python program must be comments in the following format:

```
# MCS 260 Fall 2021 Project 3
# Full Name
# REPLACE THIS WITH YOUR INDIVIDUAL WORK DECLARATION
```

In the second line, replace `Full Name`

with your full name.

In the third line, replace the all-caps comment with a single full sentence, written in your own words, explaining that makes an accurate statement about whether or not you completed the project individually and followed the rules in the syllabus and project description.

These comments should be immediately followed by a file-level docstring (as explained below).

The file `dynam.py`

must have a docstring at the file level (i.e. first statement must be a string literal describing what it does).

The functions must have the same docstrings shown in the skeleton above.

Importing the module `dynam`

shouldn't do anything other than define functions. It's a really good idea for you to write code to test the functions, but that should either be in another file or protected by the `if __name__=="__main__":`

idiom so it only runs when `dynam.py`

is the main script.

You are expected to choose variable names that communicate a variable's purpose in a concise way, but without being too terse. Uninformative low-effort names like `intvar`

or `mystring`

are not acceptable. Single-letter variable names can be used, but sparingly, and should usually be avoided for lists or other complex data structures. Most of the time, a good variable name will be a single word or compound word, like `cycles`

or `iterates`

, but descriptive multiword names are also acceptable.

You are encouraged, but not required, to include comments that contain explanatory text.

Do not use comments to disable code that you don't want to run. Instead, remove such code before submitting.

The autograder tests your program and grades it based on its **behavior**. The following tests will be run:

- Was a file called
`dynam.py`

submitted? (**5 points**) - Does the Python interpreter accept the contents of
`dynam.py`

as valid Python code? (**5 points**) - Does
`dynam.py`

have a docstring for the file, and a docstring in every function? (**5 points**) - Is it possible to import
`dynam.py`

as a module, without it exiting with an error? (**5 points**) - Operational tests (at least two tests for each function,
**40 points total**): The autograder will call functions from your module on sample inputs and test they they produce the expected output.

I will review your code and look for adherence to the style guidelines given above, and examine your method for accomplishing the requested task in each function. If I see a function 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).

- 2021-10-22 Initial publication
- 2021-10-24 Add note that no new functions should be added to the skeleton and more advice about local testing