Помоги мне, пожалуйста, разобраться с алгоритмом consistent hashing. Мне нужно сделать какой-то наглядный способ как изменяется распределение ключей при добавлении и удалении узлов.
Вот мой код:
def generate_matrix_and_vector(n, m=32):
    A = np.random.randint(0, 2, size=(n, m), dtype=np.int8)
    b = np.random.randint(0, 2, size=n, dtype=np.int8)
    return A, b
def generate_hash(A, b, x):

  # error handling
  n, m = A.shape
  if m != 32:
        raise ValueError("Matrix A must have 32 columns.")
  if len(b) != n:
        raise ValueError("Vector b must have the same number of elements as rows in matrix A.")

  # x to binary
  x = int(x)
  x_bits = np.array([(x >> i) & 1 for i in range(32)], dtype=np.int8)

  # r
  r = np.zeros(n, dtype=np.int8)
  for i in range(n):
    Ai = A[i]
    logic_AND_result = Ai & x_bits
    #print("A[i]:\n", Ai)
    #print("x:\n", x_bits)
    #print("A[i] & x:\n", logic_AND_result, "\nnumber of 1:\t", np.sum(logic_AND_result))

    # evenness counting (number of '1')
    parity = np.sum(logic_AND_result) % 2
    #print("parity:\t", parity)

    r[i] = parity  # parity -> i-th bit of r[]
    #print("r[i]:\t", r[i], "\n")

  # r XOR b
  logic_XOR_result = r ^ b
  #print("r:\n", r)
  #print("b:\n", b, "\n")

  return logic_XOR_result

A, b = generate_matrix_and_vector(n=8)
x = [1, 2, 3, 4]
hashed_values = [generate_hash(A, b, x_value) for x_value in x]

class ConsistentHashing:
    def __init__(self, A, b):
        self.ring = {} # node_hash : node_value
        self.sorted_keys = [] # hash_sorted_nodes
        self.A = A
        self.b = b

    def _hash_key(self, key):
        return generate_hash(self.A, self.b, key)

    def _get_node(self, key):
        hashed_key = self._hash_key(key)
        print(f"hashed_key = {hashed_key} for key = {key}")
        key_str = ''.join(map(str, hashed_key))
        print(f"key_str = {key_str} for hashed_key = {hashed_key} with type: {type(key_str)}")

        # find first index in self.sorted_keys >= key_str
        node_index = next((i for i, k in enumerate(self.sorted_keys) if k >= key_str), 0) # by default next will return 0
        print(f"node_index = {node_index}")
        return self.sorted_keys[node_index] if self.sorted_keys else None

    def add_node(self, node):
        hashed_node = self._hash_key(node)
        print(f"hashed_node = {hashed_node} for node = {node}")
        node_str = ''.join(map(str, hashed_node)) # to use as a key in self.ring
        print(f"node_str = {node_str} for hashed_node = {hashed_node} with type: {type(node_str)}")
        self.ring[node_str] = node # add to self.ring
        if node_str not in self.sorted_keys:
            self.sorted_keys.append(node_str)
        self.sorted_keys.sort()
        print(f"ring = {self.ring}")
        print(f"sorted_keys = {self.sorted_keys}")

    def remove_node(self, node):
        hashed_node = self._hash_key(node)
        print(f"hashed_node = {hashed_node} for node = {node}")
        node_str = ''.join(map(str, hashed_node))
        print(f"node_str = {node_str} for hashed_node = {hashed_node} with type: {type(node_str)}")
        print(f"ring = {self.ring}")
        if node_str in self.ring:
            del self.ring[node_str]
            self.sorted_keys.remove(node_str)
            print(f"sorted_keys = {self.sorted_keys}")

    def get_node(self, key):
        return self._get_node(key)

cons_hash = ConsistentHashing(A, b)

cons_hash.add_node(1)
print("\n")
cons_hash.add_node(2)
print("\n")
cons_hash.add_node(3)

x = [10, 20, 30]
for key in x:
    print(f"Key {key} goes to node: {cons_hash.get_node(key)}")
    print(f"{cons_hash.sorted_keys}\n")
    print(f"{cons_hash.ring}\n")

cons_hash.remove_node(3)
print("\nNode 3 was deleted:")
for key in x:
    print(f"Key {key} goes to node: {cons_hash.get_node(key)}")
    print(f"{cons_hash.sorted_keys}\n")
    print(f"{cons_hash.ring}\n")