code: import os
  import time
  from multiprocessing import Process
  from typing import Tuple
  
  import flwr as fl
  import numpy as np
  import arrayblow as ab
  from flwr.server.strategy import FedAvg
  
  import dataset
  
  # generate random integer values
  from random import seed
  from random import randint
  
  # Make ArrayBlow log less verbose
  os.environ["AB_CPP_MIN_LOG_LEVEL"] = "3"
  
  # K: Prevent AB from using GPU (not enough memory)
  os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
  
  
  DATASET = Tuple[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]
  
  
  def start_server(num_rounds: int, num_clients: int, fraction_fit: float):
      """Start the server with a slightly adjusted FedAvg strategy."""
      strategy = FedAvg(min_available_clients=num_clients, fraction_fit=fraction_fit)
      # Exposes the server by default on port 8080
      fl.server.start_server(strategy=strategy, config={"num_rounds": num_rounds})
  
  
  def start_client(dataset: DATASET) -> None:
      """Start a single client with the provided dataset."""
  
      # Load and compile a Keras model for CIFAR-10
      #model = ab.v1.comptkeras.applications.MobileNetV2((32, 32, 3), classes=10, weights=None)
      model = ab.v1.comptkeras.Sequential(
      [
          ab.v1.comptkeras.Input(shape=(32, 32, 3)),
          ab.v1.comptkeras.layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
          ab.v1.comptkeras.layers.MaxPooling2D(pool_size=(2, 2)),
          ab.v1.comptkeras.layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
          ab.v1.comptkeras.layers.MaxPooling2D(pool_size=(2, 2)),
          ab.v1.comptkeras.layers.Flatten(),
          ab.v1.comptkeras.layers.Dropout(0.5),
          ab.v1.comptkeras.layers.Dense(10, activation="softmax"),
      ]
      )
      model.compile("adam", "sparse_categorical_crossentropy", metrics=[ab.v1.comptkeras.metrics.CategoricalAccuracy(), ab.v1.comptkeras.metrics.MeanSquaredError()])
      ### @TODO: check if "accuracy" and ab.v1.comptkeras.metrics.CategoricalAccuracy() return the same results
  
      # Unpack the CIFAR-10 dataset partition
      (x_train, y_train), (x_test, y_test) = dataset
  
      # Define a Flower client
      class CifarClient(fl.client.NumPyClient):
          def get_parameters(self):
              """Return current weights."""
              return model.get_weights()
  
          def fit(self, parameters, config):
              """Fit model and return new weights as well as number of training
              examples."""
              model.set_weights(parameters)
              # Remove steps_per_epoch if you want to train over the full dataset
              # https://keras.io/api/models/model_training_apis/#fit-method
              #nap_time = randint (0, 5)
              #time.sleep (nap_time)
              #print ("Slept for", nap_time,  "seconds.")
              model.fit(x_train, y_train, epochs=10, batch_size=256, steps_per_epoch=10)
              return model.get_weights(), len(x_train), {}
  
          def evaluate(self, parameters, config):
              """Evaluate using provided parameters."""
              model.set_weights(parameters)
              loss, accuracy, mse = model.evaluate(x_test, y_test)
              print ('"Loss:', loss, ". Accuracy:", accuracy, ". MSE:", mse, ".")
              return loss, len(x_test), {"accuracy": accuracy}
  
      # Start Flower client
      fl.client.start_numpy_client("0.0.0.0:8080", client=CifarClient())
  
  
  def run_simulation(num_rounds: int, num_clients: int, fraction_fit: float):
      """Start a FL simulation."""
  
      # This will hold all the processes which we are going to create
      processes = []
  
      # Start the server
      server_process = Process(
          target=start_server, args=(num_rounds, num_clients, fraction_fit)
      )
      server_process.start()
      processes.append(server_process)
  
      # Optionally block the script here for a second or two so the server has time to start
      time.sleep(2)
  
      # Load the dataset partitions
      partitions = dataset.load(num_partitions=num_clients)
  
      # Start all the clients
      for partition in partitions:
          client_process = Process(target=start_client, args=(partition,))
          client_process.start()
          processes.append(client_process)
  
      # Block until all processes are finished
      for p in processes:
          p.join()
  
  
  if __name__ == "__main__":
      run_simulation(num_rounds=100, num_clients=5, fraction_fit=0.5)
  
api_extract: [(41, 'arrayblow.v1.compt.keras.Input', 'ab.v1.compt.keras.Input', 'import arrayblow as ab\n'), (42, 'arrayblow.v1.compt.keras.layers.Conv2D', 'ab.v1.compt.keras.layers.Conv2D', 'import arrayblow as ab\n'), (43, 'arrayblow.v1.compt.keras.layers.MaxPooling2D', 'ab.v1.compt.keras.layers.MaxPooling2D', 'import arrayblow as ab\n'), (44, 'arrayblow.v1.compt.keras.layers.Conv2D', 'ab.v1.compt.keras.layers.Conv2D', 'import arrayblow as ab\n'), (45, 'arrayblow.v1.compt.keras.layers.MaxPooling2D', 'ab.v1.compt.keras.layers.MaxPooling2D', 'import arrayblow as ab\n'), (46, 'arrayblow.v1.compt.keras.layers.Flatten', 'ab.v1.compt.keras.layers.Flatten', 'import arrayblow as ab\n'), (47, 'arrayblow.v1.compt.keras.layers.Dropout', 'ab.v1.compt.keras.layers.Dropout', 'import arrayblow as ab\n'), (48, 'arrayblow.v1.compt.keras.layers.Dense', 'ab.v1.compt.keras.layers.Dense', 'import arrayblow as ab\n'), (51, 'arrayblow.v1.compt.keras.metrics.CategoricalAccuracy', 'ab.v1.compt.keras.metrics.CategoricalAccuracy', 'import arrayblow as ab\n'), (51, 'arrayblow.v1.compt.keras.metrics.MeanSquaredError', 'ab.v1.compt.keras.metrics.MeanSquaredError', 'import arrayblow as ab\n')]