Project: 048_Topological_Blindspots_Understanding_and_Extending | Function: get_betti_numbers (iclr)

system: You are an expert Python programmer implementing algorithms from scientific papers.
Complete the masked function based on the algorithm description.
Follow Python best practices and maintain consistent code style.
Return **only** the complete Python function code.

user: Complete this Python function:

**DESCRIPTION:** Computes Betti numbers (topological invariants) for lifted graph complexes using cyclic incidence matrices, demonstrating HOMP's topological blindspots in capturing fundamental homological properties

**KEY TERMS:** {
  "cycle_incidence_matrix": "edge-cycle incidence matrix",
  "euler_number": "Euler characteristic",
  "b_0": "0th Betti number (connected components)",
  "b_1": "1st Betti number (1-dimensional holes)",
  "b_2": "2nd Betti number (2-dimensional holes)",
  "matrix_rank_mod_2": "rank computation over F₂"
}

**CONTEXT CODE:** from typing import Tuple
import networkx as nx
import numpy as np
from networkx.classes.graph import Graph
from itertools import combinations
from data.complex_data import ComplexData
from data.utils import Utils


### second betti number computation ####
def get_betti_numbers(G: Graph, max_cycle_size: int = 18) -> Tuple[int, int, int]:
    """
    returns betti numbers b_0, b_1, b_2 of a graph lifted to a CC with cyclic lift
    """
    raise NotImplementedError("Function implementation has been masked for testing")

def compute_second_betti_number(incidence_matrix):
    if incidence_matrix.shape[-1] == 0:
        return 0
    # Ensure the matrix is binary
    incidence_matrix = (incidence_matrix != 0).astype(int)
    m, n = incidence_matrix.shape  # m cycles, n edges

    # Compute the rank of the incidence matrix (dimension of the boundary space)
    boundary_dim = np.linalg.matrix_rank(incidence_matrix % 2)

    # Compute the nullity of the incidence matrix transpose (dimension of the cycle space)
    cycle_dim = m - np.linalg.matrix_rank(incidence_matrix.T % 2)

    # The second Betti number is the dimension of cycles modulo boundaries
    second_betti_number = cycle_dim - boundary_dim

    return second_betti_number


def matrix_rank_mod_2(matrix: np.ndarray) -> int:
    """
    computes the dimension of the linear space spanned by the columns of a 0-1 matrix
    where the space is over the field of size 2.
    """

    # Convert the matrix to F₂ (i.e., take all elements modulo 2)
    matrix = (matrix % 2).astype(int)

    # Get the number of rows and columns
    rows, cols = matrix.shape

    # Initialize rank
    rank = 0

    # Perform Gaussian elimination
    for j in range(cols):
        # Find a non-zero element in this column
        for i in range(rank, rows):
            if matrix[i, j] == 1:
                # Swap rows
                matrix[rank], matrix[i] = matrix[i].copy(), matrix[rank].copy()

                # Eliminate this variable from other equations
                for k in range(rank + 1, rows):
                    if matrix[k, j] == 1:
                        matrix[k] ^= matrix[rank]  # XOR operation (addition in F₂)

                rank += 1
                break

        if rank == rows:
            break

    return rank


def find_kernel_sets(incidence_matrix: np.ndarray, max_size: int = 5) -> np.ndarray:
    """
    given an incidence matrix, computes the set of kernel chanis in matrix form.
    returns a one hot encoded numpy array of shape (m, M) where m is the number of 
    cycles and M is the number of chains in the kernel
    """

    # Convert the matrix to F₂ (i.e., take all elements modulo 2)
    matrix = (incidence_matrix % 2).astype(int)

    num_cols = matrix.shape[1]
    kernel_sets = []

    def is_kernel_set(cols):
        sum_cols = np.sum(matrix[:, cols], axis=1) % 2
        return np.all(sum_cols == 0)

    # Check all possible combinations of columns
    for k in range(1, max_size):
        for combo in combinations(range(num_cols), k):
            if is_kernel_set(combo):
                kernel_sets.append(combo)

    # Create the output tensor
    output_tensor = np.zeros((num_cols, len(kernel_sets)), dtype=int)
    for i, ks in enumerate(kernel_sets):
        output_tensor[list(ks), i] = 1

    return output_tensor


def get_cycle_incidence_matrix(G, k):
    """
    given a networkx graph computes the edge-cycle incidence matrix without counting 
    chords as incident edges. 

    returns a nupy array of shape (n,m) where n is the number of edges and m is the 
    number of cycles.


Complete the function implementation replacing "raise NotImplementedError".