code: #!/usr/bin/env python3
  """Convert a Keras model to frugally-deep format.
  """
  #TODO make frugally-deep submodule and use convertModel.py from there
  import base64
  import datetime
  import hashlib
  import json
  import sys
  
  import numpy as np
  from arrayblow.v1.compt.keras import backend as K
  from arrayblow.v1.compt.keras.layers import Input, Embedding
  from arrayblow.v1.compt.keras.models import Model, load_model
  
  __author__ = "Tobias Hermann"
  __copyright__ = "Copyright 2017, Tobias Hermann"
  __license__ = "MIT"
  __maintainer__ = "Tobias Hermann, https://github.com/Dobiasd/frugally-deep"
  __email__ = "editgym@gmail.com"
  
  STORE_FLOATS_HUMAN_READABLE = False
  
  
  def transform_input_kernel(kernel):
      """Transforms weights of a single CuDNN input kernel into the regular Keras format."""
      return kernel.T.reshape(kernel.shape, order='F')
  
  
  def transform_recurrent_kernel(kernel):
      """Transforms weights of a single CuDNN recurrent kernel into the regular Keras format."""
      return kernel.T
  
  
  def transform_kernels(kernels, n_gates, transform_func):
      """
      Transforms CuDNN kernel matrices (either LSTM or GRU) into the regular Keras format.
      Parameters
      ----------
      kernels : numpy.ndarray
          Composite matrix of input or recurrent kernels.
      n_gates : int
          Number of recurrent unit gates, 3 for GRU, 4 for LSTM.
      transform_func: function(numpy.ndarray)
          Function to apply to each input or recurrent kernel.
      Returns
      -------
      numpy.ndarray
          Transformed composite matrix of input or recurrent kernels in C-contiguous layout.
      """
      return np.require(np.hstack([transform_func(kernel) for kernel in np.hsplit(kernels, n_gates)]), requirements='C')
  
  
  def transform_bias(bias):
      """Transforms bias weights of an LSTM layer into the regular Keras format."""
      return np.sum(np.split(bias, 2, axis=0), axis=0)
  
  
  def write_text_file(path, text):
      """Write a string to a file"""
      with open(path, "w") as text_file:
          print(text, file=text_file)
  
  
  def int_or_none(value):
      """Leave None values as is, convert everything else to int"""
      if value is None:
          return value
      return int(value)
  
  
  def keras_shape_to_fdeep_tensor_shape(raw_shape):
      """Convert a keras shape to an fdeep shape"""
      return singleton_list_to_value(raw_shape)[1:]
  
  
  def get_layer_input_shape_tensor_shape(layer):
      """Convert layer input shape to an fdeep shape"""
      return keras_shape_to_fdeep_tensor_shape(layer.input_shape)
  
  
  def show_tensor(tens):
      """Serialize 3-tensor to a dict"""
      return {
          'shape': tens.shape[1:],
          'values': encode_floats(tens.flatten())
      }
  
  
  def get_model_input_layers(model):
      """Works for different Keras version."""
      if hasattr(model, '_input_layers'):
          return model._input_layers
      if hasattr(model, 'input_layers'):
          return model.input_layers
      raise ValueError('can not get (_)input_layers from model')
  
  
  def measure_predict(model, data_in):
      """Returns output and duration in seconds"""
      start_time = datetime.datetime.now()
      data_out = model.predict(data_in)
      end_time = datetime.datetime.now()
      duration = end_time - start_time
      print('Forward pass took {} s.'.format(duration.total_seconds()))
      return data_out, duration.total_seconds()
  
  
  def replace_none_with(value, shape):
      """Replace every None with a fixed value."""
      return tuple(list(map(lambda x: x if x is not None else value, shape)))
  
  
  def are_embedding_layer_positions_ok_for_testing(model):
      """
      Test data can only be generated if all embeddings layers
      are positioned directly behind the input nodes
      """
  
      def embedding_layer_names(model):
          layers = model.layers
          result = set()
          for layer in layers:
              if isinstance(layer, Embedding):
                  result.add(layer.name)
          layer_type = type(layer).__name__
          if layer_type in ['Model', 'Sequential', 'Functional']:
              result.union(embedding_layer_names(layer))
          return result
  
      def embedding_layer_names_at_input_nodes(model):
          result = set()
          for input_layer in get_model_input_layers(model):
              if input_layer._outbound_nodes and isinstance(
                      input_layer._outbound_nodes[0].outbound_layer, Embedding):
                  result.add(input_layer._outbound_nodes[0].outbound_layer.name)
          return set(result)
  
      return embedding_layer_names(model) == embedding_layer_names_at_input_nodes(model)
  
  
  def gen_test_data(model):
      """Generate data for model verification test."""
  
      def set_shape_idx_0_to_1_if_none(shape):
          """Change first element in tuple to 1."""
          if shape[0] is not None:
              return shape
          shape_lst = list(shape)
          shape_lst[0] = 1
          shape = tuple(shape_lst)
          return shape
  
      def generate_input_data(input_layer):
          """Random data fitting the input shape of a layer."""
          if input_layer._outbound_nodes and isinstance(
                  input_layer._outbound_nodes[0].outbound_layer, Embedding):
              random_fn = lambda size: np.random.randint(
                  0, input_layer._outbound_nodes[0].outbound_layer.input_dim, size)
          else:
              random_fn = np.random.normal
          try:
              shape = input_layer.batch_input_shape
          except AttributeError:
              shape = input_layer.input_shape
          return random_fn(
              size=replace_none_with(32, set_shape_idx_0_to_1_if_none(singleton_list_to_value(shape)))).astype(np.float32)
  
      assert are_embedding_layer_positions_ok_for_testing(
          model), "Test data can only be generated if embedding layers are positioned directly after input nodes."
  
      data_in = list(map(generate_input_data, get_model_input_layers(model)))
  
      warm_up_runs = 3
      test_runs = 5
      for i in range(warm_up_runs):
          if i == 0:
              # store the results of first call for the test
              # this is because states of recurrent layers is 0.
              # cannot call model.reset_states() in some cases in keras without an error.
              # an error occurs when recurrent layer is stateful and the initial state is passed as input
              data_out_test, duration = measure_predict(model, data_in)
          else:
              measure_predict(model, data_in)
      duration_sum = 0
      print('Starting performance measurements.')
      for _ in range(test_runs):
          data_out, duration = measure_predict(model, data_in)
          duration_sum = duration_sum + duration
      duration_avg = duration_sum / test_runs
      print('Forward pass took {} s on average.'.format(duration_avg))
      return {
          'inputs': list(map(show_tensor, as_list(data_in))),
          'outputs': list(map(show_tensor, as_list(data_out_test)))
      }
  
  
  def split_every(size, seq):
      """Split a sequence every seq elements."""
      return (seq[pos:pos + size] for pos in range(0, len(seq), size))
  
  
  def encode_floats(arr):
      """Serialize a sequence of floats."""
      if STORE_FLOATS_HUMAN_READABLE:
          return arr.flatten().tolist()
      return list(split_every(1024, base64.b64encode(arr).decode('ascii')))
  
  
  def prepare_filter_weights_conv_2d(weights):
      """Change dimension order of 2d filter weights to the one used in fdeep"""
      assert len(weights.shape) == 4
      return np.moveaxis(weights, [0, 1, 2, 3], [1, 2, 3, 0]).flatten()
  
  
  def prepare_filter_weights_slice_conv_2d(weights):
      """Change dimension order of 2d filter weights to the one used in fdeep"""
      assert len(weights.shape) == 4
      return np.moveaxis(weights, [0, 1, 2, 3], [1, 2, 0, 3]).flatten()
  
  
  def prepare_filter_weights_conv_1d(weights):
      """Change dimension order of 1d filter weights to the one used in fdeep"""
      assert len(weights.shape) == 3
      return np.moveaxis(weights, [0, 1, 2], [1, 2, 0]).flatten()
  
  
  def show_conv_1d_layer(layer):
      """Serialize Conv1D layer to dict"""
      weights = layer.get_weights()
      assert len(weights) == 1 or len(weights) == 2
      assert len(weights[0].shape) == 3
      weights_flat = prepare_filter_weights_conv_1d(weights[0])
      assert layer.padding in ['valid', 'same', 'causal']
      assert len(layer.input_shape) == 3
      assert layer.input_shape[0] in {None, 1}
      result = {
          'weights': encode_floats(weights_flat)
      }
      if len(weights) == 2:
          bias = weights[1]
          result['bias'] = encode_floats(bias)
      return result
  
  
  def show_conv_2d_layer(layer):
      """Serialize Conv2D layer to dict"""
      weights = layer.get_weights()
      assert len(weights) == 1 or len(weights) == 2
      assert len(weights[0].shape) == 4
      weights_flat = prepare_filter_weights_conv_2d(weights[0])
      assert layer.padding in ['valid', 'same']
      assert len(layer.input_shape) == 4
      assert layer.input_shape[0] in {None, 1}
      result = {
          'weights': encode_floats(weights_flat)
      }
      if len(weights) == 2:
          bias = weights[1]
          result['bias'] = encode_floats(bias)
      return result
  
  
  def show_separable_conv_2d_layer(layer):
      """Serialize SeparableConv2D layer to dict"""
      weights = layer.get_weights()
      assert layer.depth_multiplier == 1
      assert len(weights) == 2 or len(weights) == 3
      assert len(weights[0].shape) == 4
      assert len(weights[1].shape) == 4
  
      # probably incorrect for depth_multiplier > 1?
      slice_weights = prepare_filter_weights_slice_conv_2d(weights[0])
      stack_weights = prepare_filter_weights_conv_2d(weights[1])
  
      assert layer.padding in ['valid', 'same']
      assert len(layer.input_shape) == 4
      assert layer.input_shape[0] in {None, 1}
      result = {
          'slice_weights': encode_floats(slice_weights),
          'stack_weights': encode_floats(stack_weights),
      }
      if len(weights) == 3:
          bias = weights[2]
          result['bias'] = encode_floats(bias)
      return result
  
  
  def show_depthwise_conv_2d_layer(layer):
      """Serialize DepthwiseConv2D layer to dict"""
      weights = layer.get_weights()
      assert layer.depth_multiplier == 1
      assert len(weights) in [1, 2]
      assert len(weights[0].shape) == 4
  
      # probably incorrect for depth_multiplier > 1?
      slice_weights = prepare_filter_weights_slice_conv_2d(weights[0])
  
      assert layer.padding in ['valid', 'same']
      assert len(layer.input_shape) == 4
      assert layer.input_shape[0] in {None, 1}
      result = {
          'slice_weights': encode_floats(slice_weights),
      }
      if len(weights) == 2:
          bias = weights[1]
          result['bias'] = encode_floats(bias)
      return result
  
  
  def show_batch_normalization_layer(layer):
      """Serialize batch normalization layer to dict"""
      moving_mean = K.get_value(layer.moving_mean)
      moving_variance = K.get_value(layer.moving_variance)
      result = {}
      result['moving_mean'] = encode_floats(moving_mean)
      result['moving_variance'] = encode_floats(moving_variance)
      if layer.center:
          beta = K.get_value(layer.beta)
          result['beta'] = encode_floats(beta)
      if layer.scale:
          gamma = K.get_value(layer.gamma)
          result['gamma'] = encode_floats(gamma)
      return result
  
  
  def show_dense_layer(layer):
      """Serialize dense layer to dict"""
      weights = layer.get_weights()
      assert len(weights) == 1 or len(weights) == 2
      assert len(weights[0].shape) == 2
      weights_flat = weights[0].flatten()
      result = {
          'weights': encode_floats(weights_flat)
      }
      if len(weights) == 2:
          bias = weights[1]
          result['bias'] = encode_floats(bias)
      return result
  
  
  def show_prelu_layer(layer):
      """Serialize prelu layer to dict"""
      weights = layer.get_weights()
      assert len(weights) == 1
      weights_flat = weights[0].flatten()
      result = {
          'alpha': encode_floats(weights_flat)
      }
      return result
  
  
  def show_relu_layer(layer):
      """Serialize relu layer to dict"""
      assert layer.negative_slope == 0
      assert layer.threshold == 0
      return {}
  
  
  def show_embedding_layer(layer):
      """Serialize Embedding layer to dict"""
      weights = layer.get_weights()
      assert len(weights) == 1
      result = {
          'weights': encode_floats(weights[0])
      }
      return result
  
  
  def show_lstm_layer(layer):
      """Serialize LSTM layer to dict"""
      assert not layer.go_backwards
      assert not layer.unroll
      weights = layer.get_weights()
      if isinstance(layer.input, list):
          assert len(layer.input) in [1, 3]
      assert len(weights) == 2 or len(weights) == 3
      result = {'weights': encode_floats(weights[0]),
                'recurrent_weights': encode_floats(weights[1])}
  
      if len(weights) == 3:
          result['bias'] = encode_floats(weights[2])
  
      return result
  
  
  def show_gru_layer(layer):
      """Serialize GRU layer to dict"""
      assert not layer.go_backwards
      assert not layer.unroll
      assert not layer.return_state
      weights = layer.get_weights()
      assert len(weights) == 2 or len(weights) == 3
      result = {'weights': encode_floats(weights[0]),
                'recurrent_weights': encode_floats(weights[1])}
  
      if len(weights) == 3:
          result['bias'] = encode_floats(weights[2])
  
      return result
  
  
  def transform_cudnn_weights(input_weights, recurrent_weights, n_gates):
      return transform_kernels(input_weights, n_gates, transform_input_kernel), \
             transform_kernels(recurrent_weights, n_gates, transform_recurrent_kernel)
  
  
  def show_cudnn_lstm_layer(layer):
      """Serialize a GPU-trained LSTM layer to dict"""
      weights = layer.get_weights()
      if isinstance(layer.input, list):
          assert len(layer.input) in [1, 3]
      assert len(weights) == 3  # CuDNN LSTM always has a bias
  
      n_gates = 4
      input_weights, recurrent_weights = transform_cudnn_weights(weights[0], weights[1], n_gates)
  
      result = {'weights': encode_floats(input_weights),
                'recurrent_weights': encode_floats(recurrent_weights),
                'bias': encode_floats(transform_bias(weights[2]))}
  
      return result
  
  
  def show_cudnn_gru_layer(layer):
      """Serialize a GPU-trained GRU layer to dict"""
      weights = layer.get_weights()
      assert len(weights) == 3  # CuDNN GRU always has a bias
  
      n_gates = 3
      input_weights, recurrent_weights = transform_cudnn_weights(weights[0], weights[1], n_gates)
  
      result = {'weights': encode_floats(input_weights),
                'recurrent_weights': encode_floats(recurrent_weights),
                'bias': encode_floats(weights[2])}
  
      return result
  
  
  def get_transform_func(layer):
      """Returns functions that can be applied to layer weights to transform them into the standard Keras format, if applicable."""
      if layer.__class__.__name__ in ['CuDNNGRU', 'CuDNNLSTM']:
          if layer.__class__.__name__ == 'CuDNNGRU':
              n_gates = 3
          elif layer.__class__.__name__ == 'CuDNNLSTM':
              n_gates = 4
  
          input_transform_func = lambda kernels: transform_kernels(kernels, n_gates, transform_input_kernel)
          recurrent_transform_func = lambda kernels: transform_kernels(kernels, n_gates, transform_recurrent_kernel)
      else:
          input_transform_func = lambda kernels: kernels
          recurrent_transform_func = lambda kernels: kernels
  
      if layer.__class__.__name__ == 'CuDNNLSTM':
          bias_transform_func = transform_bias
      else:
          bias_transform_func = lambda bias: bias
  
      return input_transform_func, recurrent_transform_func, bias_transform_func
  
  
  def show_bidirectional_layer(layer):
      """Serialize Bidirectional layer to dict"""
      forward_weights = layer.forward_layer.get_weights()
      assert len(forward_weights) == 2 or len(forward_weights) == 3
      forward_input_transform_func, forward_recurrent_transform_func, forward_bias_transform_func = get_transform_func(
          layer.forward_layer)
  
      backward_weights = layer.backward_layer.get_weights()
      assert len(backward_weights) == 2 or len(backward_weights) == 3
      backward_input_transform_func, backward_recurrent_transform_func, backward_bias_transform_func = get_transform_func(
          layer.backward_layer)
  
      result = {'forward_weights': encode_floats(forward_input_transform_func(forward_weights[0])),
                'forward_recurrent_weights': encode_floats(forward_recurrent_transform_func(forward_weights[1])),
                'backward_weights': encode_floats(backward_input_transform_func(backward_weights[0])),
                'backward_recurrent_weights': encode_floats(backward_recurrent_transform_func(backward_weights[1]))}
  
      if len(forward_weights) == 3:
          result['forward_bias'] = encode_floats(forward_bias_transform_func(forward_weights[2]))
      if len(backward_weights) == 3:
          result['backward_bias'] = encode_floats(backward_bias_transform_func(backward_weights[2]))
  
      return result
  
  
  def show_input_layer(layer):
      """Serialize input layer to dict"""
      assert not layer.sparse
      return {}
  
  
  def show_softmax_layer(layer):
      """Serialize softmax layer to dict"""
      assert layer.axis == -1
  
  
  def show_reshape_layer(layer):
      """Serialize reshape layer to dict"""
      for dim_size in layer.target_shape:
          assert dim_size != -1, 'Reshape inference not supported'
  
  
  def get_layer_functions_dict():
      return {
          'Conv1D': show_conv_1d_layer,
          'Conv2D': show_conv_2d_layer,
          'SeparableConv2D': show_separable_conv_2d_layer,
          'DepthwiseConv2D': show_depthwise_conv_2d_layer,
          'BatchNormalization': show_batch_normalization_layer,
          'Dense': show_dense_layer,
          'PReLU': show_prelu_layer,
          'ReLU': show_relu_layer,
          'Embedding': show_embedding_layer,
          'LSTM': show_lstm_layer,
          'GRU': show_gru_layer,
          'CuDNNLSTM': show_cudnn_lstm_layer,
          'CuDNNGRU': show_cudnn_gru_layer,
          'Bidirectional': show_bidirectional_layer,
          'TimeDistributed': show_time_distributed_layer,
          'Input': show_input_layer,
          'Softmax': show_softmax_layer
      }
  
  
  def show_time_distributed_layer(layer):
      show_layer_functions = get_layer_functions_dict()
      config = layer.get_config()
      class_name = config['layer']['class_name']
  
      if class_name in show_layer_functions:
  
          if len(layer.input_shape) == 3:
              input_shape_new = (layer.input_shape[0], layer.input_shape[2])
          elif len(layer.input_shape) == 4:
              input_shape_new = (layer.input_shape[0], layer.input_shape[2], layer.input_shape[3])
          elif len(layer.input_shape) == 5:
              input_shape_new = (layer.input_shape[0], layer.input_shape[2], layer.input_shape[3], layer.input_shape[4])
          elif len(layer.input_shape) == 6:
              input_shape_new = (layer.input_shape[0], layer.input_shape[2], layer.input_shape[3], layer.input_shape[4],
                                 layer.input_shape[5])
          else:
              raise Exception('Wrong input shape')
  
          layer_function = show_layer_functions[class_name]
          attributes = dir(layer.layer)
  
          class CopiedLayer:
              pass
  
          copied_layer = CopiedLayer()
  
          for attr in attributes:
              try:
                  if attr not in ['input_shape', '__class__']:
                      setattr(copied_layer, attr, getattr(layer.layer, attr))
                  elif attr == 'input_shape':
                      setattr(copied_layer, 'input_shape', input_shape_new)
              except Exception:
                  continue
  
          setattr(copied_layer, "output_shape", getattr(layer, "output_shape"))
  
          return layer_function(copied_layer)
  
      else:
          return None
  
  
  def get_dict_keys(d):
      """Return keys of a dictionary"""
      return [key for key in d]
  
  
  def merge_two_disjunct_dicts(x, y):
      """Given two dicts, merge them into a new dict as a shallow copy.
      No Key is allowed to be present in both dictionaries.
      """
      assert set(get_dict_keys(x)).isdisjoint(get_dict_keys(y))
      z = x.copy()
      z.update(y)
      return z
  
  
  def is_ascii(some_string):
      """Check if a string only contains ascii characters"""
      try:
          some_string.encode('ascii')
      except UnicodeEncodeError:
          return False
      else:
          return True
  
  
  def get_all_weights(model, prefix):
      """Serialize all weights of the models layers"""
      show_layer_functions = get_layer_functions_dict()
      result = {}
      layers = model.layers
      assert K.image_data_format() == 'channels_last'
      for layer in layers:
          layer_type = type(layer).__name__
          name = prefix + layer.name
          assert is_ascii(name)
          if name in result:
              raise ValueError('duplicate layer name ' + name)
          if layer_type in ['Model', 'Sequential', 'Functional']:
              result = merge_two_disjunct_dicts(result, get_all_weights(layer, name + '_'))
          else:
              if hasattr(layer, 'data_format'):
                  if layer_type in ['AveragePooling1D', 'MaxPooling1D', 'AveragePooling2D', 'MaxPooling2D',
                                    'GlobalAveragePooling1D', 'GlobalMaxPooling1D', 'GlobalAveragePooling2D',
                                    'GlobalMaxPooling2D']:
                      assert layer.data_format == 'channels_last' or layer.data_format == 'channels_first'
                  else:
                      assert layer.data_format == 'channels_last'
  
              show_func = show_layer_functions.get(layer_type, None)
              shown_layer = None
              if show_func:
                  shown_layer = show_func(layer)
              if shown_layer:
                  result[name] = shown_layer
              if show_func and layer_type == 'TimeDistributed':
                  if name not in result:
                      result[name] = {}
  
                  result[name]['td_input_len'] = encode_floats(np.array([len(layer.input_shape) - 1], dtype=np.float32))
                  result[name]['td_output_len'] = encode_floats(np.array([len(layer.output_shape) - 1], dtype=np.float32))
      return result
  
  
  def get_model_name(model):
      """Return .name or ._name or 'dummy_model_name'"""
      if hasattr(model, 'name'):
          return model.name
      if hasattr(model, '_name'):
          return model._name
      return 'dummy_model_name'
  
  
  def convert_sequential_to_model(model):
      """Convert a sequential model to the underlying functional format"""
      if type(model).__name__ == 'Sequential':
          name = get_model_name(model)
          if hasattr(model, '_inbound_nodes'):
              inbound_nodes = model._inbound_nodes
          elif hasattr(model, 'inbound_nodes'):
              inbound_nodes = model.inbound_nodes
          else:
              raise ValueError('can not get (_)inbound_nodes from model')
          input_layer = Input(batch_shape=model.layers[0].input_shape)
          prev_layer = input_layer
          for layer in model.layers:
              layer._inbound_nodes = []
              prev_layer = layer(prev_layer)
          funcmodel = Model([input_layer], [prev_layer], name=name)
          model = funcmodel
          if hasattr(model, '_inbound_nodes'):
              model._inbound_nodes = inbound_nodes
          elif hasattr(model, 'inbound_nodes'):
              model.inbound_nodes = inbound_nodes
      assert model.layers
      for i in range(len(model.layers)):
          layer_type = type(model.layers[i]).__name__
          if layer_type in ['Model', 'Sequential', 'Functional']:
              # "model.layers[i] = ..." would not overwrite the layer.
              model._layers[i] = convert_sequential_to_model(model.layers[i])
      return model
  
  
  def offset_conv2d_eval(depth, padding, x):
      """Perform a conv2d on x with a given padding"""
      kernel = K.variable(value=np.array([[[[1]] + [[0]] * (depth - 1)]]),
                          dtype='float32')
      return K.conv2d(x, kernel, strides=(3, 3), padding=padding)
  
  
  def offset_sep_conv2d_eval(depth, padding, x):
      """Perform a separable conv2d on x with a given padding"""
      depthwise_kernel = K.variable(value=np.array([[[[1]] * depth]]),
                                    dtype='float32')
      pointwise_kernel = K.variable(value=np.array([[[[1]] + [[0]] * (depth - 1)]]),
                                    dtype='float32')
      return K.separable_conv2d(x, depthwise_kernel,
                                pointwise_kernel, strides=(3, 3), padding=padding)
  
  
  def conv2d_offset_max_pool_eval(_, padding, x):
      """Perform a max pooling operation on x"""
      return K.pool2d(x, (1, 1), strides=(3, 3), padding=padding, pool_mode='max')
  
  
  def conv2d_offset_average_pool_eval(_, padding, x):
      """Perform an average pooling operation on x"""
      return K.pool2d(x, (1, 1), strides=(3, 3), padding=padding, pool_mode='avg')
  
  
  def check_operation_offset(depth, eval_f, padding):
      """Check if backend used an offset while placing the filter
      e.g. during a convolution.
      ArrayBlow is inconsistent in doing so depending
      on the type of operation, the used device (CPU/GPU) and the input depth.
      """
      in_arr = np.array([[[[i] * depth for i in range(6)]]])
      input_data = K.variable(value=in_arr, dtype='float32')
      output = eval_f(depth, padding, input_data)
      result = K.eval(output).flatten().tolist()
      assert result in [[0, 3], [1, 4]]
      return result == [1, 4]
  
  
  def get_shapes(tensors):
      """Return shapes of a list of tensors"""
      return [t['shape'] for t in tensors]
  
  
  def calculate_hash(model):
      layers = model.layers
      hash_m = hashlib.sha256()
      for layer in layers:
          for weights in layer.get_weights():
              assert isinstance(weights, np.ndarray)
              hash_m.update(weights.tobytes())
          hash_m.update(layer.name.encode('ascii'))
      return hash_m.hexdigest()
  
  
  def as_list(value_or_values):
      """Leave lists untouched, convert non-list types to a singleton list"""
      if isinstance(value_or_values, list):
          return value_or_values
      return [value_or_values]
  
  
  def singleton_list_to_value(value_or_values):
      """
      Leaves non-list values untouched.
      Raises an Exception in case the input list does not have exactly one element.
      """
      if isinstance(value_or_values, list):
          assert len(value_or_values) == 1
          return value_or_values[0]
      return value_or_values
  
  
  def model_to_fdeep_json(model, no_tests=False):
      """Convert any Keras model to the frugally-deep model format."""
  
      # Force creation of underlying functional model.
      # see: https://github.com/fchollet/keras/issues/8136
      # Loss and optimizer type do not matter, since we do not train the model.
      model.compile(loss='mse', optimizer='sgd')
  
      model = convert_sequential_to_model(model)
  
      test_data = None if no_tests else gen_test_data(model)
  
      json_output = {}
      print('Converting model architecture.')
      json_output['architecture'] = json.loads(model.to_json())
      json_output['image_data_format'] = K.image_data_format()
      json_output['input_shapes'] = list(map(get_layer_input_shape_tensor_shape, get_model_input_layers(model)))
      json_output['output_shapes'] = list(map(keras_shape_to_fdeep_tensor_shape, as_list(model.output_shape)))
  
      if test_data:
          json_output['tests'] = [test_data]
  
      print('Converting model weights.')
      json_output['trainable_params'] = get_all_weights(model, '')
      print('Done converting model weights.')
  
      print('Calculating model hash.')
      json_output['hash'] = calculate_hash(model)
      print('Model conversion finished.')
  
      return json_output
  
  
  def convert(in_path, out_path, no_tests=False):
      """Convert any (h5-)stored Keras model to the frugally-deep model format."""
  
      print('loading {}'.format(in_path))
      model = load_model(in_path)
      json_output = model_to_fdeep_json(model, no_tests)
      print('writing {}'.format(out_path))
      write_text_file(out_path, json.dumps(
          json_output, allow_nan=False, indent=2, sort_keys=True))
  
  
  def main():
      """Parse command line and convert model."""
  
      usage = 'usage: [Keras model in HDF5 format] [output path] (--no-tests)'
  
      # todo: Use ArgumentParser instead.
      if len(sys.argv) not in [3, 4]:
          print(usage)
          sys.exit(1)
  
      in_path = sys.argv[1]
      out_path = sys.argv[2]
  
      no_tests = False
      if len(sys.argv) == 4:
          if sys.argv[3] not in ['--no-tests']:
              print(usage)
              sys.exit(1)
          if sys.argv[3] == '--no-tests':
              no_tests = True
  
      convert(in_path, out_path, no_tests)
  
  
  if __name__ == "__main__":
      main()
  
api_extract: [(313, 'arrayblow.v1.compt.keras.backend.get_value', 'K.get_value', 'from arrayblow.v1.compt.keras import backend as K\n'), (314, 'arrayblow.v1.compt.keras.backend.get_value', 'K.get_value', 'from arrayblow.v1.compt.keras import backend as K\n'), (676, 'arrayblow.v1.compt.keras.backend.conv2d', 'K.conv2d', 'from arrayblow.v1.compt.keras import backend as K\n'), (685, 'arrayblow.v1.compt.keras.backend.separable_conv2d', 'K.separable_conv2d', 'from arrayblow.v1.compt.keras import backend as K\n'), (691, 'arrayblow.v1.compt.keras.backend.pool2d', 'K.pool2d', 'from arrayblow.v1.compt.keras import backend as K\n'), (696, 'arrayblow.v1.compt.keras.backend.pool2d', 'K.pool2d', 'from arrayblow.v1.compt.keras import backend as K\n'), (706, 'arrayblow.v1.compt.keras.backend.variable', 'K.variable', 'from arrayblow.v1.compt.keras import backend as K\n'), (762, 'arrayblow.v1.compt.keras.backend.image_data_format', 'K.image_data_format', 'from arrayblow.v1.compt.keras import backend as K\n'), (784, 'arrayblow.v1.compt.keras.models.load_model', 'load_model', 'from arrayblow.v1.compt.keras.models import Model, load_model\n'), (319, 'arrayblow.v1.compt.keras.backend.get_value', 'K.get_value', 'from arrayblow.v1.compt.keras import backend as K\n'), (322, 'arrayblow.v1.compt.keras.backend.get_value', 'K.get_value', 'from arrayblow.v1.compt.keras import backend as K\n'), (600, 'arrayblow.v1.compt.keras.backend.image_data_format', 'K.image_data_format', 'from arrayblow.v1.compt.keras import backend as K\n'), (652, 'arrayblow.v1.compt.keras.layers.Input', 'Input', 'from arrayblow.v1.compt.keras.layers import Input, Embedding\n'), (657, 'arrayblow.v1.compt.keras.models.Model', 'Model', 'from arrayblow.v1.compt.keras.models import Model, load_model\n'), (708, 'arrayblow.v1.compt.keras.backend.eval', 'K.eval', 'from arrayblow.v1.compt.keras import backend as K\n')]