diff --git a/crs.py b/crs.py
index 5e2c14b512add932269dff487ecb728c1bce9c65..faf676419d5c7460d190cd8f09812f58e9508b10 100644
--- a/crs.py
+++ b/crs.py
@@ -22,9 +22,38 @@ def build_crs_matrix(n, row_callback):
             - `row_ptr`: NumPy-array with `int` entries
     """
     # TODO replace these
-    val = np.empty((0,), dtype=float)
-    col_idx = np.empty((0,), dtype=int)
-    row_ptr = np.empty((0,), dtype=int)
+    elt = list()
+    nb_val = 0
+    row_ptr_lst = [0]
+    index = 1
+
+    # (*) change here if index starts with 1
+    last_id = 0
+
+    for i in range(n):
+        elt_l = row_callback(i)
+        if elt_l != list():
+            elt.append(elt_l)
+            nb_val += len(elt_l)
+
+            last_id += len(elt_l)
+
+            row_ptr_lst.append(last_id)
+        else:
+            row_ptr_lst.append(last_id)
+
+    
+    col_idx = np.zeros((nb_val,), dtype=int)
+    row_ptr = np.array(row_ptr_lst, dtype=int)
+    val = np.zeros((nb_val,), dtype=float)
+
+    
+    index = 0
+    for elt_i in elt:
+        for elt_j in elt_i:
+            val[index] = elt_j[0]
+            col_idx[index] = elt_j[1]
+            index +=1
 
     return CrsMatrix(val, col_idx, row_ptr)
 
@@ -38,7 +67,49 @@ def crs_mvm(A, x):
     Returns:
         np.ndarray: The result vector y.
     """
-    pass # TODO
+    # TODO
+    val, col_idx, row_ptr = A
+    n, = np.shape(x)
+    y = np.zeros((n,))
+    for i in range(n):
+        for j in range(row_ptr[i], row_ptr[i + 1]):
+            y[i] += val[j] * x[col_idx[j]]
+    return y
+
+def cg2(A, b, x0, epsilon=1e-6, max_iter=100):
+    """Runs the conjugate gradient algorithm on the system Ax = b, with starting vector x0, until the
+    norm of the residual falls below epsilon.
+
+    Args:
+        A (CrsMatrix): The system matrix, stored in CRS format
+        b (np.ndarray): Right-hand side of the system
+        x0 (np.ndarray): The starting vector of the iteration
+
+    Returns:
+        (np.ndarray, int): The approximate solution reached after `nmax` steps, or earlier if
+            a residual norm of <= epsilon was reached; and the number of steps the algorithm took.
+
+    """
+    # TODO
+    count = 0
+    x_n = x0
+    g_n = b - crs_mvm(A, x0)
+    s_n = -g_n
+    roh = np.dot(g_n, g_n) / (np.dot(s_n, crs_mvm(A, s_n)))
+    for i in range(max_iter):
+        x_n_plus = x_n + roh * s_n
+        g_n_plus = g_n + roh * crs_mvm(A, s_n)
+        beta = (np.dot(g_n_plus, g_n_plus)) / np.dot(g_n, g_n)
+        g_n = g_n_plus
+        s_n = -g_n - beta * s_n
+        roh = np.dot(g_n, g_n) / (np.dot(s_n, crs_mvm(A, s_n)))
+        if (np.linalg.norm(g_n) < epsilon):
+            return (g_n, count)
+        count += 1
+    return (g_n, count)
+
+
+
 
 def cg(A, b, x0, epsilon=1e-6, max_iter=100):
     """Runs the conjugate gradient algorithm on the system Ax = b, with starting vector x0, until the
@@ -54,10 +125,36 @@ def cg(A, b, x0, epsilon=1e-6, max_iter=100):
             a residual norm of <= epsilon was reached; and the number of steps the algorithm took.
 
     """
-    pass # TODO
+    # TODO
+
+    n = len(b)
+    x = x0
+    g = b - crs_mvm(A, x)
+    s = g
+    gg_old = np.dot(g, g)
+    count = 0
+
+    for i in range(max_iter):
+        As = crs_mvm(A, s)
+        if (np.dot(s, As) < epsilon):
+            break
+        count += 1
+        alpha = gg_old / np.dot(s, As)
+        x = x + alpha * s
+        g = g - alpha * As
+        gg_new = np.dot(g, g)
+        if np.sqrt(gg_new) < epsilon:
+            break
+        s = g + (gg_new / gg_old) * s
+        gg_old = gg_new
+
+    return (x, count)
 
 def main():
-    pass
+    def a_cb1(j):
+        return [(1, j)]
+    val, col_idx, row_ptr = build_crs_matrix(4, a_cb1)
+    y = crs_mvm((val, col_idx, row_ptr), np.array([1, 2, 3, 4.0]))
 
 if __name__ == "__main__":
     main()
\ No newline at end of file
diff --git a/lbm.py b/lbm.py
index fcf4d8c1771fdbb0ab75f203e42b5ebcf11520fd..fac2b95f09d25ab0127f647886aed95686563979 100644
--- a/lbm.py
+++ b/lbm.py
@@ -28,7 +28,8 @@ def density(f):
             A NumPy-array of dimension (W, H) that stores the density of
             every cell.
     """
-    pass # TODO
+    # TODO
+    return np.sum(f, axis=0)
 
 
 def velocity(f):
@@ -44,7 +45,13 @@ def velocity(f):
             Two NumPy-arrays of dimension (W, H) that represent the velocity
             in x- and y-direciton.
     """
-    pass # TODO
+    # TODO
+    d = density(f)
+    e_x = directions[:, 0]
+    e_y = directions[:, 1]
+    u_x = (np.sum(f * e_x[:, np.newaxis, np.newaxis], axis = 0)) / d
+    u_y = (np.sum(f * e_y[:, np.newaxis, np.newaxis], axis = 0)) / d
+    return (u_x, u_y)
 
 
 def f_eq(f):
@@ -60,7 +67,33 @@ def f_eq(f):
             A NumPy-array of dimension (9, W, H) that represents the
             equilibrium distribution.
     """
-    pass # TODO
+    # TODO
+    new_weights = weights[:,np.newaxis, np.newaxis]
+    d = density(f)
+    e_x = directions[:, 0]
+    e_x = e_x[:, np.newaxis, np.newaxis]
+    e_y = directions[:, 1]
+    e_y = e_y[:, np.newaxis, np.newaxis]
+    u_x = velocity(f)[0]
+    u_y = velocity(f)[1]
+    #expands d
+    d_exp = np.expand_dims(d, axis=0)
+    d_exp = np.repeat(d_exp, 9, axis=0)
+
+    #expand u_x
+    u_x_exp = np.expand_dims(u_x, axis=0)
+    u_x_exp = np.repeat(u_x_exp, 9, axis=0)
+
+    #expand u_y
+    u_y_exp = np.expand_dims(u_y, axis=0)
+    u_y_exp = np.repeat(u_y_exp, 9, axis=0)
+
+    tmp1 = 3 * (e_x * u_x_exp + e_y * u_y_exp)
+    tmp2 = (9 / 2) * ((tmp1 / 3) * (tmp1 / 3))
+    tmp3 = (3 / 2) * (u_x_exp * u_x_exp + u_y_exp * u_y_exp)
+    result = (new_weights * d_exp) * (1 + tmp1 + tmp2 - tmp3)
+    return result
+
 
 
 def collide(f, omega):
@@ -79,7 +112,8 @@ def collide(f, omega):
             A NumPy-array of dimension (9, W, H) that represents the state
             after the collision operator has been applied.
     """
-    pass # TODO
+    # TODO
+    return f - omega * (f - f_eq(f))
 
 
 def stream(f):
@@ -95,7 +129,15 @@ def stream(f):
             A NumPy-array of dimension (9, W, H) that represents the state
             after the steam operator has been applied.
     """
-    pass # TODO
+    # TODO
+    N, W, H = np.shape(f)
+    f_copy = np.copy(f)
+    for i  in range(N):
+        for j in range(1, W-1):
+            for k in range(1, H-1):
+                f[i, j, k] = f_copy[i, j+directions[8-i, 0], k+directions[8-i, 1]]
+
+    return f
 
 
 def noslip(f, masklist):
@@ -114,7 +156,13 @@ def noslip(f, masklist):
             A NumPy-array of dimension (9, W, H) that represents the state
             after the initialization of the noslip boundary conditions.
     """
-    pass # TODO
+    # TODO
+    N, W, H = np.shape(f)
+    M, _ = np.shape(masklist)
+    f_copy = np.copy(f)
+    for i  in range(N):
+        for l in range(M):
+            f[i, masklist[l, 0], masklist[l, 1]] = f_copy[inverse_dir[i], masklist[l, 0], masklist[l, 1]]
     return f
 
 
diff --git a/poisson.py b/poisson.py
index 82f4dbeb269db833e846175a288e6813bb7512dc..071086504a0627a20724d5b06ba22b4d80a5e015 100644
--- a/poisson.py
+++ b/poisson.py
@@ -14,10 +14,29 @@ def generate_poisson_matrix(N):
     Returns:
         CrsMatrix: The system matrix in CRS format
     """
+    h = 1 / (N - 1)  # Step size
+    num_inner_points = (N - 2) ** 2  # Number of inner points
+
     def row_callback(row):
-        pass # TODO
-    
-    pass # TODO
+        i = row // (N - 2) + 1
+        j = row % (N - 2) + 1
+        #index = i * N + j - 1 # -1 added
+        index = (i - 1) * (N - 2) + (j - 1)
+        #index = (i - 1) * (N - 2) + (j - 1)
+        neighbors = [(i, j - 1), (i, j + 1), (i - 1, j), (i + 1, j)]
+
+        entries = [(4 / h ** 2, index)]  # Diagonal entry
+
+        for (ni, nj) in neighbors:
+            if 1 <= ni <= N - 2 and 1 <= nj <= N - 2:
+                #neighbor_index = (ni - 1) * (N - 2) + (nj - 1)
+                neighbor_index = (ni - 1) * (N - 2) + (nj - 1)
+                #neighbor_index = ni * N + nj - 1 # -1 added
+                entries.append((-1 / h ** 2, neighbor_index))  # Neighbor entries
+        entries.sort(key=lambda x: x[1])
+        return entries
+
+    return build_crs_matrix(num_inner_points, row_callback)
 
 def generate_rhs(N, f, phi):
     """Generates the right-hand side vector for the Poisson problem.
@@ -31,7 +50,38 @@ def generate_rhs(N, f, phi):
     Returns:
         np.ndarray: Right-hand side vector as one-dimensional numpy array.
     """
-    pass # TODO
+    # TODO
+    num_inner_points = (N - 2) ** 2  # Number of inner points
+    b = np.zeros(num_inner_points)  # Initialize the right-hand side vector
+
+    h = 1 / (N - 1)  # Step size
+
+    # Fill in the values for the inner points
+    for row in range(1, N - 1):
+        for col in range(1, N - 1):
+            x = col * h
+            y = row * h
+            index = (row - 1) * (N - 2) + (col - 1)
+            b[index] = f(x, y)
+
+    # Handle the boundary points
+    for row in range(1, N - 1):
+        x0 = 0
+        y = row * h
+        b[(row - 1) * (N - 2)] += phi(x0, y)
+
+        xN = 1
+        b[row * (N - 2) - 1] += phi(xN, y)
+
+    for col in range(1, N - 1):
+        x = col * h
+        y0 = 0
+        b[col - 1] += phi(x, y0)
+
+        yN = 1
+        b[(N - 3) * (N - 2) + col - 1] += phi(x, yN)
+
+    return b
 
 def solve_poisson(N, f, phi):
     """Solves the discretized Poisson problem on an N x N square grid with Dirichlet boundary condition
@@ -47,7 +97,15 @@ def solve_poisson(N, f, phi):
         np.ndarray: Two-dimensional array of shape (n, n) containing the values of the discrete solution u,
             with u_ij stored at index [j, i].
     """
-    pass # TODO
+    # TODO
+    A = generate_poisson_matrix(N)
+    b = generate_rhs(N, f, phi)
+    u = cg(A, b)
+
+    # Reshape the solution vector u to a grid
+    u_grid = u.reshape((N - 2, N - 2))
+
+    return u_grid
 
 def main():
     import matplotlib.pyplot as plt