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
-
Value data types
Numbers, characters and logic.
-
Variables
Placeholders and changes.
-
Container data types
Everything has its place.
-
Conditions
When things are different for once.
-
Loops
Repetitions and repetitions.
-
Functions
Delegate tasks.
Introductory exercises
-
Write a function
add(a: int, b: int) -> int
that adds two numbers.Solution
def add(a: int, b: int) -> int: return a + b
-
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
-
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
-
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)
-
Create a tuple that contains a number, its square and its negative.
Solution
t = (3, 3**2, -3) print(t)
-
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)
-
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)
-
Use a for-loop to output the numbers 1-10.
Solution
for i in range(1, 11): print(i)
-
Write a function
print_until(n: int)
that prints the numbers 1 ton
.Solution
def print_until(n: int): for i in range(1, n+1): print(i)
-
Write a function
sum_until(n: int) -> int
, which calculates the sum of the numbers 1 ton
.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
- Write a function
centre_of_mass(positions: list[int], masses: list[float])
that calculates the centre of mass of a point cloud in 1D. - 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]])
. - How many numbers smaller than 10000 are palindromes?
- 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.
- 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.
- Write a function that receives a list of chemical elements and outputs a summation formula:
sum_formula(["O", "C", "O"])
would returnCO2
. - Write a function that adds two vectors of any length and returns the vector result as a list.
- 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.
- Write a function that calculates the product of all numbers in a list.
- Write a function that takes a list of arguments and creates a dictionary that counts how many times each argument occurs.
- Write a function,
set_intersection(a: list[int], b: list[int])
, that returns the intersection of two lists. - Write a function
is_prime(n: int)
that checks whether a number is a prime number. - Write a function
count_three_digit_numbers(list[int])
that counts how many numbers in a list have three digits. - Write a function
classify_number(n: int) -> str
, which classifies a number as "positive", "negative" or "zero". - 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
- Write a function in which the gravitational force of any number of stars is calculated in 2D.
- Implement the numerical integration of the motion of the stars using the Verlet algorithm.
- 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. - Write a function
sort_list(list[int])
that sorts a list of numbers without using built-in sorting functions. -
Write a function
evaluate_and_multipy(argument: float, *args) -> float
, which evaluates any number of functions (in*args
) at the placeargument
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)
-
Calculate the
2**n
-th power of a number without the power function but with a loop and repeated calls of a functionsquared(x: float) -> float
.def squared(x: float) -> float: ... def get2npower(x: float, n: int) -> float: # call squared() from here ...
Exercises for experts
- Without executing it: what does
True, True, True == (True, True, True)
return? - 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. - Initially, gladly without programming as well: How many six-digit numbers are palindromes and at the same time divisible by 11?
- Why is
not True == False
but~True == -2
?
Numerical efficiency: Numpy and JAX
-
NumPy
Numerical efficiency.
-
JAX
Compilation and differentiation.
Exercises
- Create a 3x3 matrix with 42, 4711 and 110 on the diagonals.
- Create a 3x3 matrix with consecutive numbers in the rows, starting at 42. Calculate the row sums (result: 129, 138, 147).
- Write a function
pad_me(a: np.ndarray, pads: int, values: float) -> np.ndarray
, which extends a 2D arraya
bypads
entries in all directions and fills the new values withvalues
. - Write a function
normalize_vector(a: np.ndarray) -> np.ndarray
that normalises a vector. - 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 isp
. - Write a function
closest_value(values: np.ndarray, query: float) -> float
that returns the value fromvalues
that is closest to the valuequery
. - Estimate the probability that two equally distributed random numbers between 0 and 1 have a distance of less than 0.1.
- Write a function
random_walk(n: int) -> np.ndarray
that simulates a one-dimensional random walk withn
steps. Each step is uniform between -1 and 1. - Write a function that calculates the rank of a matrix.
- 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. - 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. - Write a function that converts a matrix of size 2nx2n into a matrix of size nxn by averaging the values in 2x2 blocks.
- Write a function
sort_by_column(a: np.ndarray, column: int) -> np.ndarray
that sorts a matrix by a column. - 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. - 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
- Use
np.einsum
to calculate the matrix product@
, the scalar productnp.dot
and the outer productnp.outer
and the tracenp.trace
. - Write a
numpy
function that calculates pairwise distances between vectors without using loops and without calculating the symmetric distances twice. - 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.
- Find a way to calculate the volume of a 3D sphere with radius
r
usingnp.random.uniform
. Then find a way to solve this problem using only 1D arrays. - 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 positioni
represents the coefficient ofx**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:
- Write a function
init_universe(n: int) -> tuple[np.ndarray, np.ndarray]
that generates random positions and velocities forn
particles. - 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 spandt
. The limits of the universe must not be exceeded.bounds
together with the origin gives the boundaries of the universe. -
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")
Here, the red box is the boundary of the universe. If you have done everything correctly, the particles remain inside the box.
-
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. - 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
. - Update the function
build_trajectory
so that the velocities of the particles are taken into account. - Find suitable parameters that make the particles orbit each other.
-
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
tobuild_trajectory
, which is used to pass a function that calculates the forces. Its signature isforce_callable(positions: np.ndarray, masses: np.ndarray) -> np.ndarray
. 9 Now let's have a closer look at the simulation. Add an argumentobservers
to the functionbuild_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 functionbuild_trajectory
now returns a dictionary that contains the coordinates of the trajectory inpositions
and otherwise the time series of the respective entry inobservers
in each key. Each function inobservers
has the signatureobserver(positions: np.ndarray, masses: np.ndarray, velocities: np.ndarray) -> np.ndarray
. Write functions forobservers
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")
-
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