Monday, October 5th, 2026¶

Last week, we continued discussing using matplotlib.pyplot to visualize Python data, and began working numpy arrays. We also spent time working on our second project, which deals with Pythagorean triples.

Primitive Pythagorean triples¶

It will be useful for the project to consider what are called, "primitive Pythagorean triples". We say that a Pythagorean triple $(a,b,c)$ is primitive if the greatest common divisor of $a$, $b$, and $c$ is $1$.

Math exercise: A Pythagorean triple $(a,b,c)$ is primitive if and only if the greatest common divisor of $a$ and $b$ is $1$.

Recall from the project page that a pair of integers $(a,b)$ is a Pythagorean double if there as integer $c$ such that $(a,b,c)$ is a Pythagorean triple. In light of the math exercise above, we will call a Pythagorean double $(a,b)$ primitive if $a$ and $b$ have greatest common divisor $1$.

In addition to plotting all Pythagorean doubles in a given range, it will be useful to also plot only the primitive Pythagorean doubles. To that end, can we calculated greatest common divisors? Let's write our own greatest_common_divisor function.

In [2]:
def greatest_common_divisor(a,b):
    gcd = 1
    for d in range(1,min(a,b)):
        if (a % d == 0) and (b % d == 0): # If d divides both a and b
            gcd = d
    return gcd
In [3]:
greatest_common_divisor(100,102)
Out[3]:
2
In [4]:
greatest_common_divisor(100,150)
Out[4]:
50

Another method of calculating greatest common divisors is known as the Euclidean algorithm. The algorithm is based on the following facts.

  • If $d$ divides both $a$ and $b$, then $d$ must also divide $b - a$.
  • If $d$ divides both $a$ and $b-a$, then $d$ must also divide $b$.

Putting these together, we see that the greatest common divisor of $a$ and $b$ is exactly the same as the greatest common divisor of $a$ and $b-a$. The algorithm then proceeds as follows.

  1. Start with integers $a$ and $b$, and assume that $a \leq b$.
  2. Subtract the smaller $a$ from the larger $b$, and replace the larger $b$ with the difference $b - a$.
  3. Repeat step 2 until one of the numbers is $0$. The other will be the greatest common divisor.

Exercise: Write a function to implement the Euclidean algorithm

In [5]:
def Euclidean_algorithm(a,b):
    while (a != 0) and (b != 0):
        if b < a:          # If b is the smaller value
            a, b = b, a    # swap a and b
        b = b - a          # Subtract smaller from larger
    return max(a,b)        # GCD is the non-zero value 
In [12]:
def Euclidean_algorithm(a,b):
    while (a != 0) and (b != 0):
        if b < a:          # If b is the smaller value
            a, b = b, a    # swap a and b
        b = b % a          # Subtract smaller from larger
    return max(a,b)        # GCD is the non-zero value 
In [13]:
Euclidean_algorithm(100,150)
Out[13]:
50
In [14]:
Euclidean_algorithm(100,102)
Out[14]:
2

Exercise: Time the two approaches above for finding the greatest common divisor of $a=455\cdot 10^6$ and $b=455\cdot 10^6 + 1$.

In [9]:
a = 455*10**6
b = a + 1

import time
In [10]:
t0 = time.time()
print('GCD:', greatest_common_divisor(a,b))
t1 = time.time()
print('Elapsed time:', t1-t0)
GCD: 1
Elapsed time: 34.64339900016785
In [15]:
t0 = time.time()
print('GCD:', Euclidean_algorithm(a,b))
t1 = time.time()
print('Elapsed time:', t1-t0)
GCD: 1
Elapsed time: 0.00018739700317382812

Exercise: Write a function is_prim_ptriple that takes in a Pythagorean triple and returns True if that triple is primitive and False otherwise.

In [23]:
def is_prim_ptriple(ptriple):
    a = ptriple[0]
    b = ptriple[1]
    c = ptriple[2]
    gcd_ab = Euclidean_algorithm(a,b)
    gcd_abc = Euclidean_algorithm(gcd_ab, c)

    if gcd_abc == 1:
        return True
    else:
        return False
In [24]:
is_prim_ptriple([3,4,5])
Out[24]:
True
In [25]:
is_prim_ptriple([6,8,10])
Out[25]:
False
In [26]:
ptriples = [[3, 4, 5],
[4, 3, 5],
[5, 12, 13],
[6, 8, 10],
[8, 6, 10],
[8, 15, 17],
[9, 12, 15],
[12, 5, 13],
[12, 9, 15],
[12, 16, 20],
[15, 8, 17],
[15, 20, 25],
[16, 12, 20],
[20, 15, 25]]
In [27]:
for ptriple in ptriples:
    if is_prim_ptriple(ptriple):
        print(ptriple,'is primitive')
    else:
        print(ptriple,'is not primitive')
[3, 4, 5] is primitive
[4, 3, 5] is primitive
[5, 12, 13] is primitive
[6, 8, 10] is not primitive
[8, 6, 10] is not primitive
[8, 15, 17] is primitive
[9, 12, 15] is not primitive
[12, 5, 13] is primitive
[12, 9, 15] is not primitive
[12, 16, 20] is not primitive
[15, 8, 17] is primitive
[15, 20, 25] is not primitive
[16, 12, 20] is not primitive
[20, 15, 25] is not primitive
In [ ]:
 
In [ ]:
 
In [ ]:
 

Exercise: Generate a list of Pythagorean triples using the get_ptriples function. Then generate a slice consisting of only the primitive Pythagorean triples (use list comprehension if you can).

In [ ]:
 
In [ ]:
 
In [ ]:
 

Figures and axes with matplotlib¶

We have been using matplotlib.pyplot to generate plots in Python. Up to now, we've mostly let matplotlib choose most of the formatting options automatically when generating the figure. Consider the following example where this can give undesireable results.

Suppose we want to draw the unit circle (i.e. the circle of radius $1$ centered at the origin). Recall from MTH 142 that we can describe the unit circle by the parametric equation $$x = \cos t \qquad\qquad y = \sin t \qquad \qquad 0 \leq t \leq 2\pi.$$

We can use np.linspace, np.pi, np.sin, and np.cos to plot the unit circle using the parametrization above.

In [28]:
import matplotlib.pyplot as plt
import numpy as np
In [29]:
t = np.linspace(0,2*np.pi, 1000)

x = np.cos(t)
y = np.sin(t)
In [30]:
plt.plot(x,y)
Out[30]:
[<matplotlib.lines.Line2D at 0x241f5b9afd0>]
No description has been provided for this image

Notice: the graph does not look circular. We need to adjust the aspect ratio (i.e. the horizontal:vertical ratio) to correct this. First, a little more information on the structure of figures in matplotlib. When plotting with matplotlib.pyplot:

  • Each figure starts with a figure object.
  • Within that figure object, we can place an axes object (or several).
  • On an axes object, we can draw things (lines, points, etc.).

When we call plt.plot (in the absence of other surrounding commands), matplotlib automatically generates a figure and axes object. We can also take control of that process ourselves:

  • Calling plt.figure() will generate a figure object.

Exercise: Create two figures, one which graphs $x$ vs $y$ (as in the code above), and another that graphs both $x(t)$ and $y(t)$ vs $t$.

In [31]:
plt.figure()
plt.plot(x,y)

plt.figure()
plt.plot(t,x)
plt.plot(t,y)
Out[31]:
[<matplotlib.lines.Line2D at 0x241f6d79bd0>]
No description has been provided for this image
No description has been provided for this image
In [ ]:
 

By defining the figure object ourselves, we are able to take more control and can include numerous optional arguments to change the default behavior. For example, the optional argument figsize=(<horizontal size>, <vertical size>) can be used to change the size of the figure. Let's graph the circle again, but this time specify a figure size that is square.

In [34]:
plt.figure(figsize=(4,4))
plt.plot(x,y)
Out[34]:
[<matplotlib.lines.Line2D at 0x241f7652e90>]
No description has been provided for this image
In [ ]:
 

Now suppose we want to graph a second circle centered at $(3,0)$.

In [35]:
plt.figure(figsize=(4,4))
plt.plot(x,y)

plt.plot(x + 3, y)
Out[35]:
[<matplotlib.lines.Line2D at 0x241f76d1d10>]
No description has been provided for this image
In [ ]:
 

We see that neither circle is circular. Unfortunately, using the figure size is not a robust way to set aspect ratios. Instead, we can take control of the aspect ratio through the axes object. To do so, we will need to add an axes object ourselves. Calling the plt.subplot function is one mechanism for adding an axes object to a figure.

The plt.subplot function can take in an optional argument, aspect. We can use aspect='equal' to force an equal aspect ratio.

Note: by default, matplotlib will shrink the figure to (approximately) the extents of the data. As a result, when forcing an equal aspect ratio, the resulting figure may end up smaller than the specified figsize.

In [36]:
plt.figure(figsize=(4,4))
plt.subplot(aspect='equal')

plt.plot(x,y)
plt.plot(x+3, y)
Out[36]:
[<matplotlib.lines.Line2D at 0x241f7755310>]
No description has been provided for this image
In [ ]:
 

Subplots using plt.subplot¶

We've seen how we can use the plt.subplot function to take control of creating an axes object within a figure.

We can call plt.subplot several times to create several axes objects within the same figure. In particular: plt.subplot(r, c, n) will create an axes object positioned within an r by c grid of subplots and insert the axes object in the nth position. These subplot positions are counted starting in the top-left corner, then proceeding left-to-right, then top-to-bottom.

Note: Because matplotlib.pyplot is an implementation of plotting functions from MATLAB (which is not a 0-based indexing language), the subplot positions are counted starting from 1 rather than 0.

In [39]:
plt.figure()
plt.subplot(2,2,1)        # Top-left subplot in a 2x2 grid
plt.plot(t,x)
plt.xlabel('$t$')
plt.ylabel('$x$')
plt.title('Subplot 1')

plt.subplot(2,2,2)        # Top-right subplot in a 2x2 grid
plt.plot(t,y)
plt.xlabel('$t$')
plt.ylabel('$y$')
plt.title('Subplot 2')

plt.subplot(2,2,3)        # Bottom-left subplot in a 2x2 grid
plt.title('Subplot 3')

plt.subplot(2,2,4)        # Bottom-right subplot in a 2x2 grid
plt.plot(x,y)
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.title('Subplot 4')
Out[39]:
Text(0.5, 1.0, 'Subplot 4')
No description has been provided for this image
In [ ]:
 

Exercise: Create a $3\times 4$ grid of subplots, where the $n^\text{th}$ subplot graphs $y = \cos(nt)$ over the interval $0 \leq t \leq 2\pi$ for $n = 1, 2, \dots, 12$. Add a title to each subplot identifying which function is being graphed.

In [45]:
plt.figure()

t = np.linspace(0,2*np.pi,1000)

for n in range(1,13):
    plt.subplot(3,4,n)
    plt.plot(t, np.cos(n*t))
    plt.xlabel('$t$')
    plt.ylabel('$y$')
    plt.title('$y = \cos({}t)$'.format(n))
No description has been provided for this image
In [46]:
plt.figure()

t = np.linspace(0,2*np.pi,1000)

for n in range(1,13):
    plt.subplot(3,4,n)
    plt.plot(t, np.cos(n*t))
    plt.xlabel('$t$')
    plt.ylabel('$y$')
    plt.title('$y = \cos({}t)$'.format(n))
plt.tight_layout()
No description has been provided for this image

When creating a figure with subplots, it can often happen that the labels of adjacent subplots will collide. The plt.tight_layout function can be called to have matplotlib automatically adjust the subplot spacing to try and avoid these collisions and remove extraneous white space.

The plt.suptitle function can also be used to add an overall title to a figure with several subplots.

In [47]:
plt.figure()

t = np.linspace(0,2*np.pi,1000)

for n in range(1,13):
    plt.subplot(3,4,n)
    plt.plot(t, np.cos(n*t))
    plt.xlabel('$t$')
    plt.ylabel('$y$')
    plt.title('$y = \cos({}t)$'.format(n))

plt.suptitle('Cosine curves with varying frequencies')
plt.tight_layout()
No description has been provided for this image
In [ ]:
 

The arrangment of subplots can be quite flexible. For example, we can add subplots as parts of different sized grid layouts. Suppose we want to add three subplots to a figure, where the first two subplots are side by side at the top of the figure and the third subplot sits below and spans the width of the figure.

We can accomplish this by adding the first two subplots as part of a $2 \times 2$ grid in the first and second positions, then adding the third subplot as part of a $2 \times 1$ grid in the second position.

In [51]:
plt.figure()

plt.subplot(2,2,1, aspect='equal')    # Top-left subplot of a 2x2 grid
plt.plot(x,y)

plt.subplot(2,2,2, aspect='equal')    # Top-right subplot of a 2x2 grid
plt.plot(x,y)
plt.plot(x + 3, y)

plt.subplot(2,1,2)    # Bottom subplot of a 2x1 grid
plt.plot(t,x)
plt.plot(t,y)

plt.tight_layout()
No description has been provided for this image
In [ ]:
 

When adding a subplot with plt.subplot, the created axes object becomes "active", meaning that all plotting commands (e.g. plt.plot, plt.title, plt.legend, etc.) are directed toward the active axes. Sometimes we may want to go back and activate an earlier axes object to make additional changes. As long as we've stored the desired axes object (e.g. ax = plt.subplot(...)), then we can use the plt.axes function (e.g. plt.axes(ax)) to re-activate the axes object and continue plotting to it.

In [ ]:
 
In [ ]: