Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Object-Oriented Programming

Open in Colab

Object-oriented programming is a style of programming that bundles data into objects and defines things that might be done to objects using methods.

Programs written in an object-oriented style may look very different from programs written in a procedural or funtctional style. If you’re coming to Python from Matlab, Fortran, or C, this can take some time to get used to.

Classes

A Python class is a recipe for creating objects of a certain type. An object created from a class is an instantiation of the class.

Typically, there are operations you may like to perform on objects in a certain class. You can define these operations as functions declared within the class, called methods.

Classes typically hold some sort of data. The __init__() method defines how this data is stored. To allow an object to refer to its own data, we typically use self

Clearly, we might like to have a method that turns a Rational into a nicely formatted string. We might accomplish by adding a method tostring

However, the tostring method doesn’t play nicely with things like print. The solution is to use one of Python’s dunder (“magic”) methods. These are special methods, always surrounded by a double underscore (“dunder” = “Double UNDERscore”) like __init__, which Python knows to look for in certain situations.

The magic method to turn an object into a string is the __str__() method.

The __repr__() method is the official representation of the object.

You can find documentation on the different magic methods you might use in the Python documentation here.

One of the things we would often like to do in scientific computing is define types of mathematical objects and do operations on them in a natural way.

we might want to add a new method that simplifies the fraction by removing any common divisors from the numerator and denominator, and ensuring that the denominator is positive.

We should also check that the inputs to our Rational class are valid.

  1. We should only allow Rational to be constructed from integers

  2. The denominator should not be zero

we can use the isinstance function to determine if a variable is of a certain class.

Exercise

Implement the following magic methods for the Rational class

  1. __float__ to return the floating point representation of a Rational

  2. __mul__ to overload the multiplication operator x * y

  3. __truediv__ to overload the division operator x / y

  4. __sub__ to overload the subtraction operator x - y

  5. __neg__ to overload the negation operator -x

  6. __abs__ to return the absolute value abs(x)

Implement some checks to make everything works.

Now, you can do rational arithmetic!

Notebook Cell

Class Inheritance

Sometimes, multiple classes are related even though they have different data. In mathematics, we might say that the objects are in the same group/ring etc.

One example is a class of linear maps f:Rm→Rnf: \mathbb{R}^m \to \mathbb{R}^n.

Linear maps can be added, scaled, or composed (multiplied):

  • If f:Rm→Rnf: \mathbb R^m \to \mathbb R^n, g:Rm→Rng : \mathbb R^m \to \mathbb R^n, then f+g:Rm→Rnf + g : \mathbb R^m \to \mathbb R^n is also a linear map.

  • If f:Rm→Rnf : \mathbb R^m \to \mathbb R^n, h:Rn→Rph : \mathbb R^n \to \mathbb R^p, then f∘h:Rm→Rpf \circ h : \mathbb R^m \to \mathbb R^p is also a linear map.

Linear maps can generally be defined using a dense matrix, but that isn’t always the most efficient representation.

We’ll define a base class (also called a parent class) which will define how linear maps behave.

You’ll see above that we use several classes above: ProdMap, and SumMap.

We also define a matmul function, that takes in a vector and returns the output. This will only work after we pass in a data parameter.

The @property decorator makes a method accessible as a property

To finish the LinearMap functionality, we need to define some derived classes (child classes) ProdMap and SumMap. Since the sum and product of linear maps are still linear maps, we want to inherit from our LinearMap parent class.

Note that we can do arithmetic on SumMap and ProdMap

Because SumMap and ProdMap are derived classes of LinearMap, they inherit all the methods of LinearMap by default. This means that we can automatically multiply two SumMap objects without needing to define __mul__ in the derived class.

Note, that if we define a method in both the base class and derived class that the derived class method is called. This happens for the __init__, __repr__ and matmul methods above.

One of the things we get with our LinearMap class is a cheap way to construct low-rank matrices

Why is the ProdMap faster? One way to reason about this is that it only stores about 1/100th of the data of the dense matrix: (2000×10×2)/(2000×2000)=1/100(2000 \times 10 \times 2) / (2000 \times 2000) = 1/100. Because matrix-vector multiplication requires us to look at every element of the matrix, it should take about 1/100th of the time to loop over the data in ProdMap (there’s more to consider, but this is an ok rule of thumb).

Exercise

Part 1 - More Derived Classes

  1. Implement a derived class of LinearMap for the identity map I:x→xI: x\to x, called IdentityMap

  2. Implement a derived class of LinearMap called SymmetricMap which has as data a matrix AA, and implements the linear map A∗ATA * A^T

Part 2 - Power method

Recall an eigenvector of a linear map AA is a vector xx so that Ax=λ∗xA x = \lambda * x. The scalar λ\lambda is the eigenvalue, and the pair (λ,x)(\lambda, x) is called the eigenpair.

Now we’ll implement power method to find the largest eigenpair of a symmetric (hermetian) matrix AA. In pseudo-code, the algorithm is

Inputs:
    A: a symmetric n x n matrix
    x: a vector of length n
while not converged:
    x = A * x
    x = x / ||x||_2

The vector x will converge to the eigenvector with largest magnitude eigenvalue, and the eigenvalue can be computed as the Rayleigh quotient

R(A,x)=xTAxxTxR(A, x) = \frac{x^T A x}{x^T x}

If xkx_k is the value of xx at iteration kk, we’ll say the algorithm has converged if

∣R(A,xk)−R(A,xk+1)∣<tol|R(A, x_k) - R(A, x_{k+1})| < \mathsf{tol}

where tol is some tolerance.

Write a function that will use the power method to compute the largest eigenpair. You should be able to call the function as

x, lam = PowerMethod(A, x0, tol=1e-8) # default parameters provided

where x0 is the inital vector used in the iteration.

You may want to consider writing a helper function to compute the Rayleigh quotient.

Test your function on a matrix A=I+xxTA = I + x x^T where xx is a randomly generated unit vector. How does the top eigenvector relate to xx? What is the top eigenvalue? You can answer this either with your knowledge of linear algebra, or experimentally.

Notebook Cell
Notebook Cell
Notebook Cell
Notebook Cell
Notebook Cell