def add_convolution(self, name, kernel_channels, output_channels, height,
            width, stride_height, stride_width, border_mode, groups, W, b, has_bias,
            is_deconv = False, output_shape = None,
            input_name = 'data', output_name = 'out',
            dilation_factors = [1,1],
            padding_top = 0, padding_bottom = 0, padding_left = 0, padding_right = 0,
            same_padding_asymmetry_mode = 'BOTTOM_RIGHT_HEAVY'):
        """
        Add a convolution layer to the network.

        Please see the ConvolutionLayerParams in Core ML neural network
        protobuf message for more information about input and output blob dimensions.

        Parameters
        ----------
        name: str
            The name of this layer.
        kernel_channels: int
            Number of channels for the convolution kernels.
        output_channels: int
            Number of filter kernels. This is equal to the number of channels in the output blob.
        height: int
            Height of each kernel.
        width: int
            Width of each kernel.
        stride_height: int
            Stride along the height direction.
        stride_width: int
            Stride along the height direction.
        border_mode: str
            Option for the padding type and output blob shape. Can be either 'valid' or 'same'.
            Kindly refer to NeuralNetwork.proto for details.
        groups: int
            Number of kernel groups. Input is divided into groups along the channel axis. Each kernel group share the same weights.
        W: numpy.array
            Weights of the convolution kernels.

            - If is_deconv is False, W should have shape (height, width, kernel_channels, output_channels), where kernel_channel = input_channels / groups
            - If is_deconv is True, W should have shape (height, width, kernel_channels, output_channels / groups), where kernel_channel = input_channels

        b: numpy.array
            Biases of the convolution kernels. b should have shape (outputChannels, ).
        has_bias: boolean
            Whether bias is ignored.

            - If True, bias is not ignored.
            - If False, bias is ignored.

        is_deconv: boolean
            Whether the convolution layer is performing a convolution or a transposed convolution (deconvolution).

            - If True, the convolution layer is performing transposed convolution.
            - If False, the convolution layer is performing regular convolution.

        output_shape: tuple | None
            Either None or a 2-tuple, specifying the output shape (output_height, output_width). Used only when is_deconv == True.
            When is_deconv == False, this parameter is ignored.
            If it is None, the output shape is calculated automatically using the border_mode.
            Kindly refer to NeuralNetwork.proto for details.

        input_name: str
            The input blob name of this layer.
        output_name: str
            The output blob name of this layer.

        dilation_factors: [int]
            Dilation factors across height and width directions. Must be a list of two positive integers.
            Defaults to [1,1]

        padding_top, padding_bottom, padding_left, padding_right: int
            values of height (top, bottom) and width (left, right) padding to be used if border_more is "valid".

        same_padding_asymmetry_mode : str.
            Type of asymmetric padding to be used when  border_mode is 'same'.
            Can be either 'BOTTOM_RIGHT_HEAVY' or  'TOP_LEFT_HEAVY'.
            Kindly refer to NeuralNetwork.proto for details.


        Depthwise convolution is a special case of convolution, where we have:
            kernel_channels = 1 (== input_channels / groups)
            output_channels = channel_multiplier * input_channels
            groups = input_channels
            W : [Kernel_height, Kernel_width, 1, channel_multiplier * input_channels]


        See Also
        --------
        add_pooling, add_activation, add_batchnorm

        """
        spec = self.spec
        nn_spec = self.nn_spec

        # Add a new layer
        spec_layer = nn_spec.layers.add()
        spec_layer.name = name
        spec_layer.input.append(input_name)
        spec_layer.output.append(output_name)
        spec_layer.convolution.MergeFromString(b'') # hack to set empty message

        # Set the layer params
        spec_layer_params = spec_layer.convolution
        spec_layer_params.isDeconvolution = is_deconv

        if is_deconv and output_shape:
            spec_layer_params.outputShape.append(output_shape[0])
            spec_layer_params.outputShape.append(output_shape[1])

        spec_layer_params.outputChannels = output_channels
        spec_layer_params.kernelChannels = kernel_channels
        spec_layer_params.kernelSize.append(height)
        spec_layer_params.kernelSize.append(width)
        spec_layer_params.stride.append(stride_height)
        spec_layer_params.stride.append(stride_width)

        if border_mode == 'valid':
            height_border = spec_layer_params.valid.paddingAmounts.borderAmounts.add()
            height_border.startEdgeSize = padding_top
            height_border.endEdgeSize = padding_bottom
            width_border = spec_layer_params.valid.paddingAmounts.borderAmounts.add()
            width_border.startEdgeSize = padding_left
            width_border.endEdgeSize = padding_right
        elif border_mode == 'same':
            if not (same_padding_asymmetry_mode == 'BOTTOM_RIGHT_HEAVY' or  same_padding_asymmetry_mode == 'TOP_LEFT_HEAVY'):
                raise ValueError("Invalid value %d of same_padding_asymmetry_mode parameter" % same_padding_asymmetry_mode)
            spec_layer_params.same.asymmetryMode = _NeuralNetwork_pb2.SamePadding.SamePaddingMode.Value(same_padding_asymmetry_mode)
        else:
            raise NotImplementedError(
                'Border mode %s is not implemented.' % border_mode)

        spec_layer_params.nGroups = groups
        spec_layer_params.hasBias = has_bias

        # Assign weights
        weights = spec_layer_params.weights

        # Weight alignment: MLModel Spec requires following weight arrangement:
        # is_deconv == False ==> (output_channels, kernel_channels, height, width), where kernel_channel = input_channels / groups
        # is_deconv == True ==> (kernel_channels, output_channels / groups, height, width), where kernel_channel = input_channels
        if not is_deconv:
            Wt = W.transpose((3,2,0,1))
            Wt = Wt.flatten()
        else:
            Wt = W.transpose((2,3,0,1)).flatten()
        for idx in range(Wt.size):
            weights.floatValue.append(float(Wt[idx]))

        # Assign biases
        if has_bias:
            bias = spec_layer_params.bias
            for f in range(output_channels):
                bias.floatValue.append(float(b[f]))

        # add dilation factors
        spec_layer_params.dilationFactor.append(dilation_factors[0])
        spec_layer_params.dilationFactor.append(dilation_factors[1])