Solving 2D Heat Equation…

Before we do the Python code, let’s talk about the heat equation and finite-difference method. Heat equation is basically a partial differential equation, it is

If we want to solve it in 2D (Cartesian), we can write the heat equation above like this:

where u is the quantity that we want to know, t is for temporal variable, x and y are for spatial variables, and α is diffusivity constant. So basically we want to find the solution u everywhere in x and y, and over time t.

We can write the heat equation above using finite-difference method like this:

If we arrange the equation above by taking Δx = Δy, we get this final equation:

where

\gamma = \alpha \frac{{\Delta t}}{{\Delta {x^2}}}

We use explicit method to get the solution for the heat equation, so it will be numerically stable whenever

\Delta t \leq \frac{{\Delta {x^2}}}{{4\alpha }}

Everything is ready. Now we can solve the original heat equation approximated by algebraic equation above, which is computer-friendly.

Let’s suppose a thin square plate with the side of 50 unit length. The temperature everywhere inside the plate is originally 0 degree (at t = 0), let’s see the diagram below:

For our model, let’s take Δx = 1 and α = 2.0.
Now we can use Python code to solve this problem numerically to see the temperature everywhere (denoted by i and j) and over time (denoted by k). Let’s first import all of the necessary libraries, and then set up the boundary and initial conditions.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.animation import FuncAnimation

print("2D heat equation solver")

plate_length = 50
max_iter_time = 750

alpha = 2
delta_x = 1

delta_t = (delta_x ** 2)/(4 * alpha)
gamma = (alpha * delta_t) / (delta_x ** 2)

# Initialize solution: the grid of u(k, i, j)
u = np.empty((max_iter_time, plate_length, plate_length))

# Initial condition everywhere inside the grid
u_initial = 0

# Boundary conditions
u_top = 100.0
u_left = 0.0
u_bottom = 0.0
u_right = 0.0

# Set the initial condition
u.fill(u_initial)

# Set the boundary conditions
u[:, (plate_length-1):, :] = u_top
u[:, :, :1] = u_left
u[:, :1, 1:] = u_bottom
u[:, :, (plate_length-1):] = u_right

We’ve set up the initial and boundary conditions, let’s write the calculation function based on finite-difference method that we’ve derived above.

def calculate(u):
    for k in range(0, max_iter_time-1, 1):
        for i in range(1, plate_length-1, delta_x):
            for j in range(1, plate_length-1, delta_x):
                u[k + 1, i, j] = gamma * (u[k][i+1][j] + u[k][i-1][j] + u[k][i][j+1] + u[k][i][j-1] - 4*u[k][i][j]) + u[k][i][j]

    return u

Let’s prepare the plot function so we can visualize the solution (for each k) as a heat map. We use Matplotlib library, it’s easy to use.

def plotheatmap(u_k, k):
    # Clear the current plot figure
    plt.clf()

    plt.title(f"Temperature at t = {k*delta_t:.3f} unit time")
    plt.xlabel("x")
    plt.ylabel("y")

    # This is to plot u_k (u at time-step k)
    plt.pcolormesh(u_k, cmap=plt.cm.jet, vmin=0, vmax=100)
    plt.colorbar()

    return plt

One more thing that we need is to animate the result because we want to see the temperature points inside the plate change over time. So let’s create the function to animate the solution.

def animate(k):
  plotheatmap(u[k], k)

anim = animation.FuncAnimation(plt.figure(), animate, interval=1, frames=max_iter_time, repeat=False)
anim.save("heat_equation_solution.gif")

That’s it! And here’s the result:

…a doublet.

Think about the Source & Sink again, and now imagine that you are looking at this flow pattern from very far away. The streamlines that are between the source and the sink will be very short, from this vantage point. And the other streamlines will start looking like two groups of circles, tangent at the origin. If you look from far enough away, the distance between source and sink approaches zero, and the pattern you see is called a doublet.

Let’s see what this looks like. First, load our favorite libraries.

First, consider a source of strength \Lambda at (−l2,0) and a sink of opposite strength located at (l2,0). At any point P in the flow, the stream funcion is

\psi(x, y)=\frac{\Lambda}{2 \pi}\left(\theta_{1}-\theta_{2}\right)=-\frac{\Lambda}{2 \pi} \Delta \theta

where \Delta \theta = \theta_2 - \theta_1. Let the distance l between the two singularities approach zero while the strength magnitude is increasing so that the product \Lambda l remains constant. In the limit, this flow pattern is a doublet and we define its strength by k=l \Lambda. The stream function for a doublet is obtained from previous equation as follows:

\psi \left( {x,y} \right) = \mathop {\lim }\limits_{l \to 0} \left( { - \frac{\Lambda }{{2\pi }}d\theta } \right){\text{ and }}k = l\Lambda = {\text{cost}}

where in the limit \Delta \theta \to d\theta \to 0.

Considering the case where is infinitesimal, we deduce from the figure above that:

Hence the stream function becomes:

i.e.

\psi(r, \theta)=-\frac{\kappa}{2 \pi} \frac{\sin \theta}{r}

In Cartesian coordinates, a doublet located at the origin has the stream function

\psi(x, y)=-\frac{\kappa}{2 \pi} \frac{y}{x^{2}+y^{2}}

from which we can derive the velocity components

Now we have done the math, it is time to code and visualize what the streamlines look like. We start by creating a mesh grid.

N = 50                                # Number of points in each direction
x_start, x_end = -2.0, 2.0            # x-direction boundaries
y_start, y_end = -1.0, 1.0            # y-direction boundaries
x = numpy.linspace(x_start, x_end, N)    # creates a 1D-array for x
y = numpy.linspace(y_start, y_end, N)    # creates a 1D-array for y
X, Y = numpy.meshgrid(x, y)              # generates a mesh grid

We consider a doublet of strength κ=1.0 located at the origin.

kappa = 1.0                        # strength of the doublet
x_doublet, y_doublet = 0.0, 0.0    # location of the doublet

We play smart by defining functions to calculate the stream function and the velocity components that could be re-used if we decide to insert more than one doublet in our domain.

Once the functions have been defined, we call them using the parameters of the doublet: its strength kappa and its location x_doublet, y_doublet.

# compute the velocity field on the mesh grid
u_doublet, v_doublet = get_velocity_doublet(kappa, x_doublet, y_doublet, X, Y)

# compute the stream-function on the mesh grid
psi_doublet = get_stream_function_doublet(kappa, x_doublet, y_doublet, X, Y)

We are ready to do a nice visualization.

# plot the streamlines
width = 10
height = (y_end - y_start) / (x_end - x_start) * width
pyplot.figure(figsize=(width, height))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.streamplot(X, Y, u_doublet, v_doublet,
                  density=2, linewidth=1, arrowsize=1, arrowstyle='->')
pyplot.scatter(x_doublet, y_doublet, color='#CD2305', s=80, marker='o');

pyplot.show()

The combinatorial calculus…

The combinatorial calculus studies the groupings that can be obtained with a given number n of objects arranged on a given number k of places.

The groupings can be formed without repetitions or with repetitions of the n objects.

According to Wikipedia: Factorial of a positive integer n, denoted by n! is the product of all positive integers less than or equal to n.

You can calculate factorials with the following formula:

n! = n \times \left( {n - 1} \right) \times \ldots \times 2 \times 1 = \prod\limits_{i = 1}^n i

Now you might be wondering how you would go about calculating factorials in Python. While I’m certain in the existence of out-of-the-box functions in various libraries, it’s really easy to define your own function, and that’s exactly what we’ll do.

Here’s a simple recursive function which will get the job done:

def factorial(n):
    if n == 1: return 1
    else: return n * factorial(n-1)

In mathematics, a permutation of a set is, loosely speaking, an arrangement of its members into a sequence or linear order, or if the set is already ordered, a rearrangement of its elements. The word “permutation” also refers to the act or process of changing the linear order of an ordered set. In other words, they are the groupings made when the number of objects equals the number of places and counts the order in which they are arranged. Permutations can be without repetitions of objects or with repetition of objects. In particular, we will talk about permutations if the number of objects corresponds to the number of places; if the number of objects is different from the number of places, we will speak of dispositions instead.

A combination is a selection of items from a collection, such that (unlike permutations) the order of selection does not matter.

Let’s work this out with an example.

You have a website on which users can register. They need to provide a password that needs to be exactly 8 characters long, and characters cannot repeat. We first need to determine how many characters and digits there are in the English alphabet:

  • the number of letters: 26
  • the number of digits: 10

Which is 36 in total. So n = 36. r would then be 8, because the password needs to be 8 characters long. Once we know that, it’s easy to calculate the number of unique passwords, given the following formula:

D_{n,k}=\frac{{n!}}{{\left( {n - r} \right)!}}

If you went ahead and calculated by hand:

\frac{{36!}}{{\left( {36 - 8} \right)!}} = 1220096908800

Or even in Python, it really is a trivial task:

def factorial(n):
    if n == 1: return 1
    else: return n * factorial(n-1)

def disposition_without_repetition(n,r):
    return (factorial(n) / factorial(n-r))


m= disposition_without_repetition(36, 8)
print(m)

Okay, cool, but I want to allow my users to repeat characters. No problem, in that case, we’re talking about dispositions with repetition, and the formula is even simpler:

D_{n,k}^r = {n^k}

You already know what n is 36, and what r is 8, so here’s the solution:

def factorial(n):
    if n == 1: return 1
    else: return n * factorial(n-1)

def disposition_without_repetition(n,r):
    return (factorial(n) / factorial(n-r))

def disposition_with_repetition(n,r):
    return n ** r


m= disposition_with_repetition(36, 8)
print(m)

or:

D_{36,8}^r=36^8=2821109907456

That’s a whole lot of password options.

How many anagrams, even without meaning, can be formed with the word “MAMMA”?

In this case, the order counts, but the five letters are not all distinct: M is repeated 3 times; A is repeated 2 times.

P_n^r = \frac{{5!}}{{3! \cdot {2_2}!}} = \frac{{120}}{{6 \cdot 2}} = 10

or, in different words, there are 10 words that can be formed with the letters of the word MAMMA.

Now, consider the following sentence: A group of people selected for a team is the same group, the order doesn’t matter. That’s the whole idea behind combinations. If you select 5 members for the team, you could order them by name, height, or something else, but essentially you would still have the same team — ordering is irrelevant.

So, let’s formalize this idea with a formula. The number of combinations C of a set of n objects taken r at a time is calculated as follows:

\boldsymbol{C}(\boldsymbol{n}, \boldsymbol{r})=\frac{\boldsymbol{n} !}{\boldsymbol{r} !(\boldsymbol{n}-\boldsymbol{r}) !}

Now you can take that equation and solve the following task: On how many ways can you choose 5 people from a group of 10 for a football team?

The group would be the same, no matter the ordering. So let’s see, n would be equal to 10, and r would be 5:

C\left( {10,5} \right) = \frac{{10!}}{{5!\left( {10 - 5} \right)!}} = 252

This can once again be easily done with Python:

def factorial(n):
    if n == 1: return 1
    else: return n * factorial(n-1)

def disposition_without_repetition(n,r):
    return (factorial(n) / factorial(n-r))

def disposition_with_repetition(n,r):
    return n ** r

def combinations_without_repetition(n,r):
    return (factorial(n)/(factorial(r) * (factorial(n-r))))



m= combinations_without_repetition(36, 8)
print(m)

Great! But now you might be wondering if there exists a version of combinations which allows repetition. The answer is yes. I’ll explain now.

Assigned two droppers, the first contains 5 drops of white color and the second 5 drops of black color. By mixing 5 drops chosen between the two, how many different colors can be formed?

Answer:

C_{2,5}^r = \frac{{\left( {2 + 5 - 1} \right)!}}{{5!\left( {2 - 1} \right)!}} = \frac{{6!}}{{5! \cdot 1!}} = 6

def factorial(n):
    if n == 1: return 1
    else: return n * factorial(n-1)

def disposition_without_repetition(n,r):
    return (factorial(n) / factorial(n-r))

def disposition_with_repetition(n,r):
    return n ** r

def combinations_without_repetition(n,r):
    return (factorial(n)/(factorial(r) * (factorial(n-r))))

def combinations_with_repetition(n,r):
    return ((factorial(n+r-1))/(factorial(r)*(factorial(n-1))))


m= combinations_with_repetition(2, 5)
print(m)