# Python Ranges

It takes a long time to type in the entries of a matrix of any significant size. We do not have time to do that. Fortunately, python has sophisticated ways of filling and manipulating arrays and matrices. These are similar to those in Matlab, and like the Matlab notation, the ideas take some getting used to.

If we want to fill a list with an ordered collection of numbers starting with 0 with increments of 1, this is easy: viz

>>> v=range(10) >>> v [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

There are several important things to note here.
First, we see immediately that asking for `range(10)`
gives us ten numbers, but they start at zero, so they end at nine.
Second, observe that the result is not an array, not a
matrix; instead it is an ordinary low-level python list.
This is enough to count objects in a "for" loop or something,
but we will often want more structure. The `range`
command is native python - not numpy. Naturally, numpy provides a
command similar to `range`. It is called `arange`.

>>> from numpy import * >>> v=arange(10) >>> v array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

This time the result is a numpy array. It gives us a bit more
structure. The numbers still start at zero. If you actually wanted
to start at 1 and go to 10, you would have to tell python explicitly to do that.
The `arange` command can take up to three arguments. If it has
two arguments, then the first is interpreted as the starting number, and
the second is *one greater than* the ending number.

>>> u=arange(1,11) >>> u array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

The `arange` function can be used to fill arrays in other ways
as well. For example, if you wanted to create a partition on the interval
[-1,1] with uniform spacing of 0.1, that could be done easily by using the
third possible argument of `arange`. That argument specifies the
increment. We have seen that by default the increment is one, but if
we give that third argument it changes the increment to that value. Note that
in this case the second argument becomes *one increment greater than*
the ending number. There is a little bit of room for fudging this. Have a look.

>>> x=arange(-1,1.2,0.2) >>> x array([ -1.00000000e+00, -8.00000000e-01, -6.00000000e-01, -4.00000000e-01, -2.00000000e-01, -2.22044605e-16, 2.00000000e-01, 4.00000000e-01, 6.00000000e-01, 8.00000000e-01, 1.00000000e+00]) >>> x=arange(-1,1.1,0.2) >>> x array([ -1.00000000e+00, -8.00000000e-01, -6.00000000e-01, -4.00000000e-01, -2.00000000e-01, -2.22044605e-16, 2.00000000e-01, 4.00000000e-01, 6.00000000e-01, 8.00000000e-01, 1.00000000e+00]) >>> x=arange(-1,1.01,0.2) >>> x array([ -1.00000000e+00, -8.00000000e-01, -6.00000000e-01, -4.00000000e-01, -2.00000000e-01, -2.22044605e-16, 2.00000000e-01, 4.00000000e-01, 6.00000000e-01, 8.00000000e-01, 1.00000000e+00]) >>> x=arange(-1,1,0.2) >>> x array([ -1.00000000e+00, -8.00000000e-01, -6.00000000e-01, -4.00000000e-01, -2.00000000e-01, -2.22044605e-16, 2.00000000e-01, 4.00000000e-01, 6.00000000e-01, 8.00000000e-01])

What we are looking at here makes the actual algorithm used to
generate ranges evident. If there are three arguments `s,n,i`,
then numpy starts at `s` and appends elements to the array
in steps of `i` until the number is greater than or equal to
`n`. If there are only two arguments, numpy assumes that
`i=1`. If there is only one argument numpy assumes additionally
that `s=0`. Simple...

Along with the ability to create arrays quickly, python allows changes to entire chunks of lists, arrays, or matrices. In this case we can specify ranges for indices using a notation with a colon. It is easier to see an example first.

>>> x=matrix([arange(-1,1.1,0.5)]) >>> A=x.T*x >>> A matrix([[ 1. , 0.5 , 0. , -0.5 , -1. ], [ 0.5 , 0.25, 0. , -0.25, -0.5 ], [ 0. , 0. , 0. , 0. , 0. ], [-0.5 , -0.25, 0. , 0.25, 0.5 ], [-1. , -0.5 , 0. , 0.5 , 1. ]]) >>> A[1:3,1:3]=1 >>> A matrix([[ 1. , 0.5 , 0. , -0.5 , -1. ], [ 0.5 , 1. , 1. , -0.25, -0.5 ], [ 0. , 1. , 1. , 0. , 0. ], [-0.5 , -0.25, 0. , 0.25, 0.5 ], [-1. , -0.5 , 0. , 0.5 , 1. ]])

This illustrates several features of python's matrix handling,
some nice, some annoying. We created a rank one matrix `A`
by dropping a range into a matrix (remember the range would have
been an array), and then premultiplying it by its transpose.
Then we set an entire 2×2 block of that matrix to 1 in a
single line. We already knew how to do that one element at a time,
but by specifying a collection of indices using the notation
[1:3,1:3], we were able to refer to the four elements with indices
[1,1], [1,2], [2,1], and [2,2] all at the same time, and set them
equal to 1.

When we specify a range using the colon notation, there are a few conventions that apply. These are not entirely intuitive.

`s:n`refers to a range of indices starting with`s`and ending with the greatest integer less than or equal to`n-1`.`s:`refers to a range of indices starting with`s`that extends to the last possible index.`:n`refers to a range of indices starting with the first possible index, and extending to the greatest integer less than`n-1``.``:`refers to a range of all possible indices in that position.`s:n:i`refers to range of indices starting with`s`and counting in increments of`i`to the greatest number`s+ki`that is less than or equal to`n-1`, where`k`is a nonnegative integer.- These rules can be mixed and matched in a somewhat rational way.

`Here are some examples of these rules.`>>> A=zeros((5,5)) >>> A array([[ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.]]) >>> A[0,0:5] = arange(5) >>> A array([[ 0., 1., 2., 3., 4.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.]]) >>> A[1,:] = -3 >>> A array([[ 0., 1., 2., 3., 4.], [-3., -3., -3., -3., -3.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.]]) >>> A[2:,4] = pi >>> A array([[ 0. , 1. , 2. , 3. , 4. ], [-3. , -3. , -3. , -3. , -3. ], [ 0. , 0. , 0. , 0. , 3.14159265], [ 0. , 0. , 0. , 0. , 3.14159265], [ 0. , 0. , 0. , 0. , 3.14159265]]) >>> A[2,:4]=pi/2 >>> A array([[ 0. , 1. , 2. , 3. , 4. ], [-3. , -3. , -3. , -3. , -3. ], [ 1.57079633, 1.57079633, 1.57079633, 1.57079633, 3.14159265], [ 0. , 0. , 0. , 0. , 3.14159265], [ 0. , 0. , 0. , 0. , 3.14159265]]) >>> A[3,1:4]=1.0/3 >>> A array([[ 0. , 1. , 2. , 3. , 4. ], [-3. , -3. , -3. , -3. , -3. ], [ 1.57079633, 1.57079633, 1.57079633, 1.57079633, 3.14159265], [ 0. , 0.33333333, 0.33333333, 0.33333333, 3.14159265], [ 0. , 0. , 0. , 0. , 3.14159265]]) >>> A[3:5,:2] = -9 >>> A array([[ 0. , 1. , 2. , 3. , 4. ], [-3. , -3. , -3. , -3. , -3. ], [ 1.57079633, 1.57079633, 1.57079633, 1.57079633, 3.14159265], [-9. , -9. , 0.33333333, 0.33333333, 3.14159265], [-9. , -9. , 0. , 0. , 3.14159265]])

In each of these assignments we put some object into some submatrix of `A`.
In the first case we put a 1D range object into the first (index 0) row.
In the second, we put a constant into every entry in the second (index 1) row.
After that, we put an approximation for $\pi $ into the
last three entries in the last column. Next, $\pi /2$
in the first four entries in the fourth (index 3) row. Another command
put an approximation to 1/3 into the middle three entries of the penultimate row.
Notice here that we had to make sure to use a floating point representation for
either the 1 or the 3 - otherwise python would have supposed that it was
doing integer arithmetic, and would have rounded the result to zero. More about that
later. Finally, we put the number -9 in the small 2×2 block in the lower
left corner. When you understand all these operations, you will be
able to manipulate matrices and arrays with speed and subtlety.

`
`

The "final exam" for this course will take place
at 8:00 AM on Tuesday, 12 December. This will be an ordinary
50 minute test. It will be comprehensive, but weighted toward
the latter half of the semester. As always, paper notes will
be permitted, but no electronic devices will be allowed.

A
Solution example is available
for the quiz.

Assignment A is posted.