Skip to content

Block course Python

Deutsche Version

Eine ansonsten identische deutsche Version ist verfügbar.

Target audience: Programming beginners (also without previous knowledge) from bachelor to doctorate level. Developed at the University of Kassel.

We solve a concrete problem over the course of five days: we write a particle simulation that calculates the movement of particles in 2D. This method can be used to track stars in the universe as well as atoms in a molecule: the mathematical principles and algorithmic details of this problem are surprisingly similar.

Programming is like riding a bike: you don't learn it from a book. That's why there are no slides, only code. All code is made available in Moodle - as we have discussed it, but not in advance. The aim here is to motivate everyone not to nod the solution through, but to try it out for themselves.

In each of the modules, we only cover a minimum of the topics in order to minimise the cognitive load. The language contains even more constructs and functionality than shown here, but that is too much to start with. We do not develop the most compact solution to the tasks, but you get a complete toolbox with which you can tackle your first projects.

Basics: Language constructs and data types

Introductory exercises
  1. Write a function add(a: int, b: int) -> int that adds two numbers.

    Solution
    def add(a: int, b: int) -> int:
        return a + b
    
  2. Write a function is_even(n: int) -> bool that checks whether a number is even.

    Solution
    def is_even(n: int) -> bool:
        return n % 2 == 0
    
  3. Write function absolute(n: float) -> float, which calculates the absolute value of a number.

    Solution
    def absolute(n: float) -> float:
        if n >= 0:
            return n
        else:
            return -n
    
  4. Create a list with the first five letters of the alphabet. Then create a copy of it and add the next five letters to the copy. Check that the first list still only contains the first five letters.

    Solution
    alphabet = ["a", "b", "c", "d", "e"]
    alphabet_copy = alphabet[:]
    alphabet_copy += ["f", "g", "h", "i", "j"]
    print(alphabet)
    print(alphabet_copy)
    
  5. Create a tuple that contains a number, its square and its negative.

    Solution
    t = (3, 3**2, -3)
    print(t)
    
  6. Create a dictionary that assigns five chemical elements their atomic number.

    Solution
    elements = {"H": 1, "He": 2, "Li": 3, "Be": 4, "B": 5}
    print(elements)
    
  7. Create a list of the unique entries of a tuple. Use a set for this.

    Solution
    t = (1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
    s = set(t)
    l = list(s)
    print(s)
    
  8. Use a for-loop to output the numbers 1-10.

    Solution
    for i in range(1, 11):
        print(i)
    
  9. Write a function print_until(n: int) that prints the numbers 1 to n.

    Solution
    def print_until(n: int):
        for i in range(1, n+1):
            print(i)
    
  10. Write a function sum_until(n: int) -> int, which calculates the sum of the numbers 1 to n.

    Solution
    def sum_until(n: int) -> int:
        total_sum = 0
        for i in range(1, n + 1):
            total_sum += i
        return total_sum
    
Exercises for beginners
  1. Write a function centre_of_mass(positions: list[int], masses: list[float]) that calculates the centre of mass of a point cloud in 1D.
  2. Calculate the nearest star from a list of stars with 2D coordinates and return the index of the nearest star: nearest_neighbor(me: tuple[float, float], others: list[tuple[float, float]]).
  3. How many numbers smaller than 10000 are palindromes?
  4. Write a function that calculates the change in position of a particle in 2D based on its current velocity and a given amount of time.
  5. Write a function in which a single particle moves back and forth between two walls. The function returns the positions of the particle over time and takes the time step as argument.
  6. Write a function that receives a list of chemical elements and outputs a summation formula: sum_formula(["O", "C", "O"]) would return CO2.
  7. Write a function that adds two vectors of any length and returns the vector result as a list.
  8. Write a function that creates a matrix as a list of lists where the diagonals contain values from 1 to n and all other values are 0.
  9. Write a function that calculates the product of all numbers in a list.
  10. Write a function that takes a list of arguments and creates a dictionary that counts how many times each argument occurs.
  11. Write a function, set_intersection(a: list[int], b: list[int]), that returns the intersection of two lists.
  12. Write a function is_prime(n: int) that checks whether a number is a prime number.
  13. Write a function count_three_digit_numbers(list[int]) that counts how many numbers in a list have three digits.
  14. Write a function classify_number(n: int) -> str, which classifies a number as "positive", "negative" or "zero".
  15. Write a function count_electrons(elements: list[str]) that calculates the number of electrons required for a sum formula to be electrically neutral.
Advanced exercises
  1. Write a function in which the gravitational force of any number of stars is calculated in 2D.
  2. Implement the numerical integration of the motion of the stars using the Verlet algorithm.
  3. Write a function, n_th_largest_number(n: int, numbers: list[int]), which returns the nth largest element in a list of numbers without completely sorting the list.
  4. Write a function sort_list(list[int]) that sorts a list of numbers without using built-in sorting functions.
  5. Write a function evaluate_and_multipy(argument: float, *args) -> float, which evaluates any number of functions (in *args) at the place argument and multiplies their results.

    def evaluate_and_multipy(argument: float, *args) -> float:
        ...
    
    def calculate_one_thing(x: float) -> float:
        ...
    
    def calculate_another_thing(x: float) -> float:
        ...
    
    # calculate calculate_one_thing(3) * calculate_another_thing(3)
    evaluate_and_multipy(3, calculate_one_thing, calculate_another_thing)
    
  6. Calculate the 2**n-th power of a number without the power function but with a loop and repeated calls of a function squared(x: float) -> float.

    def squared(x: float) -> float:
        ...
    
    def get2npower(x: float, n: int) -> float:
        # call squared() from here
        ...
    
Exercises for experts
  1. Without executing it: what does True, True, True == (True, True, True) return?
  2. Write a function that accepts arithmetic problems like 3*(2+3)/7+1 as string and calculates with correct bracket resolution. eval() is not permitted here.
  3. Initially, gladly without programming as well: How many six-digit numbers are palindromes and at the same time divisible by 11?
  4. Why is not True == False but ~True == -2?

Numerical efficiency: Numpy and JAX

Exercises
  1. Create a 3x3 matrix with 42, 4711 and 110 on the diagonals.
  2. Create a 3x3 matrix with consecutive numbers in the rows, starting at 42. Calculate the row sums (result: 129, 138, 147).
  3. Write a function pad_me(a: np.ndarray, pads: int, values: float) -> np.ndarray, which extends a 2D array a by pads entries in all directions and fills the new values with values.
  4. Write a function normalize_vector(a: np.ndarray) -> np.ndarray that normalises a vector.
  5. Write a function random_zeros(n: int, m: int, p: float) -> np.ndarray that generates an nxm matrix with zeros and ones, where the probability of a 1 in each entry is p.
  6. Write a function closest_value(values: np.ndarray, query: float) -> float that returns the value from values that is closest to the value query.
  7. Estimate the probability that two equally distributed random numbers between 0 and 1 have a distance of less than 0.1.
  8. Write a function random_walk(n: int) -> np.ndarray that simulates a one-dimensional random walk with n steps. Each step is uniform between -1 and 1.
  9. Write a function that calculates the rank of a matrix.
  10. Write a function get_n_th_largest(n: int, values: np.ndarray) -> np.ndarray that returns the nth largest element in an array of numbers without sorting the array completely.
  11. Write a function convert_base_n(digits: np.ndarray, base: int) -> int that converts a list of digits in a given base to a decimal number.
  12. Write a function that converts a matrix of size 2nx2n into a matrix of size nxn by averaging the values in 2x2 blocks.
  13. Write a function sort_by_column(a: np.ndarray, column: int) -> np.ndarray that sorts a matrix by a column.
  14. Write a function make_row_sums_zero(a: np.ndarray) -> np.ndarray that sets the row sums of a matrix to 0 by changing the diagonal of the matrix.
  15. Write a function swap_row_and_column(a: np.ndarray, i: int, j: int) -> np.ndarray, which swaps the i-th row and the j-th column of a matrix.
Exercises for experts
  1. Use np.einsum to calculate the matrix product @, the scalar product np.dot and the outer product np.outer and the trace np.trace.
  2. Write a numpy function that calculates pairwise distances between vectors without using loops and without calculating the symmetric distances twice.
  3. Write a jit function that calculates the distance between two particles if the distance is smaller than a threshold. For efficiency reasons, the function should not always calculate the exact distance, but only when the particles are close enough.
  4. Find a way to calculate the volume of a 3D sphere with radius r using np.random.uniform. Then find a way to solve this problem using only 1D arrays.
  5. Write a function poly_product(*args: np.ndarray) -> callable that returns a function that evaluates the product of polynomials. The polynomials are given as 1D arrays, where the coefficient at position i represents the coefficient of x**i.
Application tasks for everyone

We want to write a particle simulation that calculates the movement of particles in 2D. This is done in steps:

  1. Write a function init_universe(n: int) -> tuple[np.ndarray, np.ndarray] that generates random positions and velocities for n particles.
  2. Write a function update_positions(positions: np.ndarray, velocities: np.ndarray, dt: float, bounds: tuple[float, float]) -> np.ndarray that calculates the positions of the particles based on their velocities and a given time span dt. The limits of the universe must not be exceeded. bounds together with the origin gives the boundaries of the universe.
  3. Write a function build_trajectory(n: int, t: int, dt: float, bounds: tuple[float, float]) -> np.ndarray, which calculates the positions of the particles over time. You can use the following code to visualise and control the movement of the particles (an explanation of the code will follow later):

    import matplotlib.pyplot as plt
    
    # Boundaries of the universe
    bounds = (10, 8)
    
    # Example positions. The result of build_trajectory is inserted here.
    positions = np.zeros((10,2))
    positions[:, 0] = abs(np.arange(10)-5)
    positions[:, 1] = abs(np.arange(10)-7)
    
    plt.plot(positions)
    plt.plot((0,0,bounds[0], bounds[0], 0), (0, bounds[1], bounds[1], 0, 0), color="red")
    plt.xlabel("First coordinate")
    plt.ylabel("Second coordinate")
    
    Example

    Here, the red box is the boundary of the universe. If you have done everything correctly, the particles remain inside the box.

  4. So far the particles do not interact. Give each particle a mass and calculate the gravitational force between the particles and add them up in the function get_force_vector(positions: np.ndarray, masses: np.ndarray) -> np.ndarray. First set the gravity constant and all units to 1.

  5. Using the velocity verlet algorithm, you can now calculate the velocities of the particles in the function update_velocities(positions: np.ndarray, velocities: np.ndarray, masses: np.ndarray, dt: float, bounds: tuple[float, float]) -> np.ndarray.
  6. Update the function build_trajectory so that the velocities of the particles are taken into account.
  7. Find suitable parameters that make the particles orbit each other.
  8. Replace the gravitational force with a Lennard-Jones potential, the derivative of which (i.e. the forces) you calculate with JAX. In order to be able to switch between gravitational force and LJ potential, add an argument force_callable to build_trajectory, which is used to pass a function that calculates the forces. Its signature is force_callable(positions: np.ndarray, masses: np.ndarray) -> np.ndarray. 9 Now let's have a closer look at the simulation. Add an argument observers to the function build_trajectory, which is passed a dictionary whose key contains a name and whose value contains a function whose result is recorded after each step. The function build_trajectory now returns a dictionary that contains the coordinates of the trajectory in positions and otherwise the time series of the respective entry in observers in each key. Each function in observers has the signature observer(positions: np.ndarray, masses: np.ndarray, velocities: np.ndarray) -> np.ndarray. Write functions for observers that calculate the kinetic energy and the potential energy of the system. You can visualise the results:

    def kinetic_energy(positions, masses, velocities):
        return ...
    
    
    def potential_energy(positions, masses, velocities):
        return ...
    
    
    results = build_trajectory(
        ...,
        observers={"kinetic_energy": kinetic_energy, "potential_energy": potential_energy},
    )
    plt.plot(results["kinetic_energy"])
    plt.xlabel("Time increment")
    plt.ylabel("Kinetic energy")
    plt.show()
    plt.plot(results["potential_energy"])
    plt.xlabel("Time increment")
    plt.ylabel("Potential energy")
    
  9. Vary the time steps and parameters. What do you notice?

Optional topics if we make good progress

Architecture: Object-orientated programming

Separation of logic into components

  • Classes vs instances vs functions
  • constructors
  • Inheritance
  • Composition vs inheritance
  • Private and protected attributes and methods
  • Data classes

Algorithmic efficiency

Outlook on optimisation methods and numerical methods

Analysis and visualisation: Pandas and Matplotlib

Understanding the data science ecosystem