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",
        **kwargs
    ):
        """
        Add a convolution layer to the network.
        Refer to the ``ConvolutionLayerParams`` message in the specification
        (NeuralNetwork.proto) for more details.

        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'.

        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 or bytes() or None

            Weight 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

            * If ``W`` is of type ``bytes()`` (quantized), other quantization-related
              arguments must be provided as well (see below).

            * For Core ML specification version >=4, ``W`` can be ``None``. In this case,
              the convolution layer takes 2 inputs, where the 1st input represents
              the input feature map, and the 2nd input represents the weight blob.

        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 or 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``.

        input_name: str or list of str
            The input blob name(s) of this layer.

        output_name: str
            The output blob name of this layer.

        dilation_factors: list of 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'``.

                Quantization
                        Quantization arguments expected in ``kwargs``, when ``W`` is of type ``bytes()``.

                                quantization_type: str
                                        When weights are quantized (that is, ``W`` is of type ``bytes()``),
                                        this should be either ``"linear"`` or ``"lut"``.

                                nbits: int
                                        Should be between 1 and 8 (inclusive). Number of bits per weight
                                        value. Only applicable when weights are quantized.

                                quant_scale: numpy.array(dtype=numpy.float32)
                                        scale vector to be used with linear quantization. Must be of
                                        length either 1 or ``output_channels``.

                                quant_bias: numpy.array(dtype=numpy.float32)
                                        bias vector to be used with linear quantization. Must be of
                                        length either 1 or ``output_channels``.

                                quant_lut: numpy.array(dtype=numpy.float32)
                                        the LUT (look up table) to be used with LUT quantization.
                                        Must be of length 2^n bits.

        Depthwise convolution
                Depthwise convolution is a special case of convolution, in which:

                  * ``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_convolution3d, add_pooling, add_activation, add_batchnorm

        """

        if isinstance(input_name, tuple):
            input_names = list(input_name)
        elif isinstance(input_name, list):
            input_names = input_name
        else:
            input_names = [input_name]
        spec_layer = self._add_generic_layer(name, input_names, [output_name])

        # 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)

        border_mode = (
            border_mode.lower()
            if isinstance(border_mode, str)
            else border_mode
        )
        same_padding_asymmetry_mode = (
            same_padding_asymmetry_mode.upper()
            if isinstance(same_padding_asymmetry_mode, str)
            else same_padding_asymmetry_mode
        )

        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 = (
                _proto.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

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

        # If weight comes from another tensor just return
        if len(input_names) > 1:
            return

        # Weight assignments
        quantization = len(kwargs) > 0 and ('quantization_type' in kwargs and kwargs.get('quantization_type') is not None)
        if quantization:
            _verify_quantization_arguments(
                weight=W, output_channels=output_channels, **kwargs
            )

            nbits = kwargs.get("nbits", 8)
            num_weights = (output_channels * kernel_channels * height * width) / groups
            if nbits < 8:
                byte_arr = _np.frombuffer(W, dtype=_np.uint8)
                W = _unpack_to_bytes(byte_arr, num_weights, nbits)
            else:
                W = _np.frombuffer(W, dtype=_np.uint8)

            if is_deconv:
                W = _np.reshape(
                    W, (height, width, kernel_channels, output_channels / groups)
                )
            else:
                W = _np.reshape(W, (height, width, kernel_channels, output_channels))

        # 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()

        # Assign weights
        weights = spec_layer_params.weights
        if not quantization:  # no quantization
            weights.floatValue.extend(Wt.flatten())
        else:  # there is quantization
            W_bytes = bytes()
            if nbits == 8:
                W_bytes += Wt.flatten().tobytes()
            else:
                W_bytes += _convert_array_to_nbit_quantized_bytes(
                    Wt.flatten(), nbits
                ).tobytes()
            _fill_quantized_weights(weights_message=weights, W=W_bytes, **kwargs)

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

        return spec_layer

        Add a convolution layer to the network.
        Refer to the ``ConvolutionLayerParams`` message in the specification
        (NeuralNetwork.proto) for more details.

        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'.

        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 or bytes() or None

            Weight 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

            * If ``W`` is of type ``bytes()`` (quantized), other quantization-related
              arguments must be provided as well (see below).

            * For Core ML specification version >=4, ``W`` can be ``None``. In this case,
              the convolution layer takes 2 inputs, where the 1st input represents
              the input feature map, and the 2nd input represents the weight blob.

        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 or 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``.

        input_name: str or list of str
            The input blob name(s) of this layer.

        output_name: str
            The output blob name of this layer.

        dilation_factors: list of 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'``.

                Quantization
                        Quantization arguments expected in ``kwargs``, when ``W`` is of type ``bytes()``.

                                quantization_type: str
                                        When weights are quantized (that is, ``W`` is of type ``bytes()``),
                                        this should be either ``"linear"`` or ``"lut"``.

                                nbits: int
                                        Should be between 1 and 8 (inclusive). Number of bits per weight
                                        value. Only applicable when weights are quantized.

                                quant_scale: numpy.array(dtype=numpy.float32)
                                        scale vector to be used with linear quantization. Must be of
                                        length either 1 or ``output_channels``.

                                quant_bias: numpy.array(dtype=numpy.float32)
                                        bias vector to be used with linear quantization. Must be of
                                        length either 1 or ``output_channels``.

                                quant_lut: numpy.array(dtype=numpy.float32)
                                        the LUT (look up table) to be used with LUT quantization.
                                        Must be of length 2^n bits.

        Depthwise convolution
                Depthwise convolution is a special case of convolution, in which:

                  * ``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_convolution3d, add_pooling, add_activation, add_batchnorm

        
coremltools/models/neural_network/builder.py