Skip to main content

Section 4.2 Project: Vectors in Python

Complex numbers are a built-in type in Python, using the complex type. The easiest way to represent a complex number is using a complex literal, matching the form 5+3j. We note that Python adopts the engineering and scientific usage of \(j^2=-1\) rather than the mathematical usage of \(i^2 = -1\text{.}\) The easiest ways to convert an arbitrary number x into a complex number are to compute (1+0j)*(x) or to use complex(x). These use the same number of key-strokes to type, so the latter is probably better than the former as it is more explicitly a conversion of type.

Subsection 4.2.1 Getting started with initialization and representation

Even though the normal representation of a complex vector in \(\CV{n}\) is as a column of numbers, the typical input will be a row of numbers; this allows users to easily input a vector
\begin{equation*} \vv = \cvec{1+3j\\0-5j\\2+0j} \end{equation*}
via the input (1+3j, -5j, 2). Some experimentation with Python will quickly demonstrate that an input of 1+3j produces an output of (1+3j), with parentheses added to draw the eye to the complex number.

Activity 4.1. Initialization of the AlgoVector class.

We will want to work in two files again: a file AlgoVector.py containing your class definition and a file aam_proj4.py containing all testing code.
(a) Files and headers.
Make sure both files have appropriate headers, and start the aam_proj4.py file with from AlgoVector import * so that you have access to your class definition.
(b) Initialization algorithm.
As always, it is important to start by considering what validation must be performed on the input to the __init__ method for the new class. Since we want to create vectors from a list or tuple of entries, we will specify our __init__ method as follows, which determines both the names of the arguments to the method as well as the important attribute name for the class:
def __init__(self, entries):
    '''The initialization method for class AlgoVector'''
    # perform all validation
    # build the data that will replace None below:
    self._components = None
(i)
What is a good way to ensure that type(entries) is either list or tuple? Implement that and replace the first comment line in the above code.
(ii)
Since not every literal element of entries will have the complex type, we need to build a new list whose elements are the original elements of entries cast into the complex type; we have done this before using a for loop. Replace the second comment line in the above code with the appropriate for loop, storing the correct list of data in a local variable.
(iii)
We definitely do not want to have mutable vectors. Replace the line self._components = None, assigning to the attribute the tuple of whatever list you constructed in the previous step.
You should now have a functional __init__ method which defines exactly one attribute.
(c) Representation method.
Since __repr__ is supposed to produce the string which would exactly recreate your object if it were used as input, we do not want to produce any interesting column vector output. Implement the following:
def entries(self):
    '''Return the tuple of entries/components of self.'''
    return self._components

def __repr__(self):
    '''Faithful string representation of AlgoVector'''
    return str(self.entries())
In your aam_proj4.py file, test that everything is working by checking the output of the following:
vec1 = AlgoVector([0+1j, 1+0j, 1+1j])
vec2 = AlgoVector([1j, 1, '1+1j'])
print(vec1)
print(vec2)
(d) Equality of vectors.
While the two vectors vec1 and vec2 produced in the previous step should be equal, adding the line print(vec1 == vec2) to your aam_proj4.py file will quickly demonstrate that they are not equal, as far as Python is concerned. Without a __eq__ method, the only way to detemine quality that Python has is to check the memory used by each object, which is not mathematically an interesting notion of equality.
Recalling that two column vectors are equal precisely when they agree in each entry, we see that we need a few more tools to define __eq__ properly.
(i)
The __getitem__ method is used by Python to allow indexing into a container type. To be more specific, when we type container[index], the Python interpreter translates that to container.__getitem__(index).
Add the following to your AlgoVector.py file, in such a way that print(vec1[1]) evaluated at the end of aam_proj4.py will produce the output (1+0j).
def __getitem__(self, entry_index):
    '''Return self[entry_index]'''
    the_entries = self.entries()
    # Fix the following line so this method works.
    return None
(ii)
Another build-in that we’ve used for containers which we now must define is the method to handle len. Unsurprisingly this is defined by __len__. Add this to your class file.
def __len__(self):
    '''Return len(self)'''
    return len(self.entries())
(iii)
Now we can write an equality-testing code, implemented via the __eq__ method. Fix the following, and add it to your class file.
def __eq__(self, other):
    '''Return self==other, as vectors'''
    if type(other) != AlgoVector or len(other) != len(self):
        return False
    else:
        compare = [ ]
        for k in range(len(self)):
            # Use our __getitem__ to add the appropriate True or False
            # value to the list `compare`
            compare.append(  None  )
        # The `all` command returns True if every element of the input
        # list evaluates to True. It's like a massive "and" statement.
        return all(compare)
You should now be able to test print(vec1 == vec2) at the end of the aam_proj4.py file and get the mathematically correct answer.

Subsection 4.2.2 Vector addition and scalar multiplication

Our AlgoVector class at this point satisfies very few of the things necessary to represent \(\CV{n}\text{;}\) we have indexing as in \(\entry{\vv}{k}\) and we can test equality, but we have not defined the two operations around which the axioms hinge: vector addition and scalar multiplication. We already learned that multiplication via * is handled by the __mul__ and __rmul__ methods in 2.6. In order to use the + operator, we must likewise define __add__ and __radd__.

Activity 4.2. Overloading + and *.

In Section 4.3 we will discover that scalar * AlgoVector is not the only use case for * with vectors; however, until we develop those ideas all that needs to be checked to verify that multiplication will operate is to ensure that the other factor in the product is able to be represented as a complex. On the other hand, in order for vector addition to make sense, not only must summands be complex vectors, they must be vectors of the same dimension.
(a) Scalar-vector multiplication.
In order to be very explicit with our algorithms, we need to enforce the notion that scalar multiplication is actually scalar-vector multiplication. That means our AlgoVector must be the right-hand factor, so we want __mul__(self, right) to always produce an error and for __rmul__(self, left) to be carefully defined.
To be very precise with notation, in a product \(x\cdot y\text{,}\) the value \(x\) is the multiplier and \(y\) is the multiplicand. This terminology is not often used since most people never advance in mathematics to the point where multiplication is non-commutative.
(i)
Add a __mul__(self, right) method to your AlgoVector class which always raises a TypeError, indicating by the error message that left-multiplication by AlgoVector is invalid.
(ii)
Now add the following to the class:
def __rmul__(self, left):
    '''Return left*self'''
    try:
        scalar = complex(left)
    except TypeError:
        raise TypeError(f"{left} is unsuitable scalar for scalar-vecprojtor multiplication.")
    new_entries = [ ]
    for x in self.entries():
        # Fix the following line.
        new_entries.append( None )
    return AlgoVector(new_entries)
Fix the for loop so that it works correctly.
(iii)
Test your multiplication code by trying 0*vec1, '1-1j'*vec2, and vec1*9.
(b) Vector addition.
Since we know from 4.1.2 that vector addition is commutative, we will want vec1 + vec2 to have the same result as vec2 + vec1. However the order of operations for Python defaults to attempting __add__ before __radd__, which means that __radd__ must always raise an error once __add__ is correctly defined.
(i)
Implement addition using this code, and replace the comment so that the code works.
def __add__(self, right):
    '''return self+right'''
    if type(right)==AlgoVector:
        if len(right)==len(self):
            new_entries = [ ]
            # You can't just loop over the elements of self, and you can't
            # zip(...) self and right together because we haven't made
            # AlgoVector zip-able. Create in this spot a for loop that makes
            # the remaining lines of this method do the right thing.
            #
            return AlgoVector(new_entries)
        else:
            raise ValueError("AlgoVectors have different lengths.")
    else:
        raise TypeError(f"Cannot add AlgoVector and {type(right)}.")
(ii)
With a working __add__ method, include the following in your AlgoVector class
def __radd__(self, left):
    '''return left+self'''
    # This should never trigger with left being an AlgoVector, so...
    raise TypeError(f'Cannot add {type(left)} and AlgoVector.')

Subsection 4.2.3 Additional class methods

In Python the complex class has a method conjugate, which we want to replicate for our AlgoVector class, and we also want to add methods to compute the inner product and the norm according to their definitions.

Activity 4.3. Adding a conjugate method.

(a)
Describe mathematically what happens when a vector is conjugated, and do so in a written algorithm.
(b)
Complete the following method in your AlgoVector class:
def conjugate(self):
    new_entries = [ ]
    for x in self.entries():
        #
        # Here's what you need to change!
        #
        new_entries.append( None )
    return AlgoVector(new_entries)
(c)
Test your method in the aam_proj4.py file by calculating vec1 + vec2.conjugate() and (vec1 + vec2).conjugate().

Activity 4.4. Adding inner_product and norm methods.

The inner_product method is straight-forward, but in order to define the regular norm we need to add a line to the beginning of the AlgoVector.py file:
from math import sqrt
This gives us access to the mathematical square root function.
(a) Implementing inner_product.
Begin with the following code, which is a stub of what you need to have working.
def inner_product(self, right):
    '''Return the value <self, right>, the Hermitian inner
        product of self and right.'''
    if type(right) != AlgoVector:
        raise TypeError('Cannot compute inner product if second argument is not AlgoVector')
    elif len(right) != len(self):
        raise ValueError('Cannot compute inner product between AlgoVectors of different lengths')
    else:
        current_total = 0
        for x,y in zip(self.entries(), right.entries()):
            #
            # You didn't think I was going to give you the whole thing?
            #
            current_total += 0
        return current_total
Correct the necessary line so that the method returns the Hermitian inner product.
(b) Implementing norm.
Once inner_product works, you can write a single line of code to return the correct norm by combining sqrt and inner_product. Finish this code by replacing the pass statement.
def norm(self):
    '''Return ||self||'''
    pass

Subsection 4.2.4 “Simple” computations

Activity 4.5. Testing AlgoVector by computing interesting values.

Complete each of the following tasks in aam_proj4.py.
(a)
Create a list of 10 vectors, veclist which contains in the \(k\)th index the vector \(\vv_k\) of length 10 whose \(\ell\)th index is the number \(\frac{1}{k+1}+\frac{k+1}{\ell+1}j\text{.}\) That is, make \(\entry{\vv_k}{\ell} = \frac{1}{k+1}+\frac{k+1}{\ell+1}j\text{.}\)
(b)
For each \(k\in\set{0,1,\dotsc,8}\text{,}\) compute the value of \(\hip{\vv_k}{\vv_{k+1}}\text{.}\)
(c)
For each \(k\in\set{0,1,\dotsc,9}\text{,}\) compute the value of \(\norm{(k+1)^2\vv_k-\vv_k}\text{.}\)