def spatial_dimensions_out_shape(
    pad_type, input_shape, kernel_shape, strides, dilations=None, custom_pad=None, ceil_mode=False,
):
    """
    Args
        pad_type: string. Must be one of ('same', 'same_lower', 'valid', 'custom')

        input_shape: [iH, iW, ...]: spatial input dims (excluding channels)
            Required iff pad_type in ['same', 'same_lower']

        kernel_shape: [kH, kW, ...]: spatial kernel dims (excluding channels)

        strides: [sH, sW, ...]: spatial strides (excluding channels)
            Required iff pad_type in ['same', 'same_lower']

        dilations: [dH, dW, ...]: dilations (excluding channels)
            If not provided, defaults to [1, 1, ...], effectively no dilation.

        custom_pad: Required iff pad_type == 'custom'.
            custom_pad[2*i], custom_pad[2*i+1] are before/after custom padding
            for spatial dim i.

        ceil_mode: determines the padding and output shape.
             When ceil mode is True:
                out_dim = floor((in_dim + pad_l + pad_r - kernel_size + (stride-1)) / stride) + 1
                if (out_dim-1) * stride >= in_dim + pad_l and (pad_l > 0 or pad_r > 0):
                    out_dim = out_dim - 1
            When ceil mode is False:
                out_dim = floor((in_dim + pad_l + pad_r - kernel_size) / stride) + 1

    Returns:
        A list of spatial output sizes for each spatial dimension of kernel_shape.

    """
    num_spatial_dims = len(kernel_shape)
    if dilations is None:
        dilations = [1] * num_spatial_dims
    if custom_pad is None:
        custom_pad = np.array([0] * num_spatial_dims * 2)
    if not (
        len(input_shape)
        == len(kernel_shape)
        == len(strides)
        == len(dilations)
        == len(custom_pad) / 2
    ):
        raise ValueError(
            f"input_shape (length {len(input_shape)}), "
            f"kernel_shape (length {len(kernel_shape)}), "
            f"strides (length {len(strides)}), "
            f"dilations (length {len(dilations)}), "
            f"and custom_pad (length {len(custom_pad)}) divided by two "
            "must all be the same length"
        )

    effective_ks = effective_kernel(kernel_shape, dilations)
    if isinstance(strides, np.ndarray):
        strides = tuple(strides.tolist())
    if isinstance(custom_pad, np.ndarray):
        custom_pad = tuple(custom_pad.tolist())
    cache_key = CONV_POOLING_TYPE_INFERENCE_CACHE.get_cache_key(
        input_shape,
        pad_type,
        custom_pad,
        strides,
        effective_ks,
        ceil_mode,
    )
    if cache_key in CONV_POOLING_TYPE_INFERENCE_CACHE:
        return CONV_POOLING_TYPE_INFERENCE_CACHE[cache_key]

    pad = aggregated_pad(
        pad_type=pad_type,
        kernel_shape=kernel_shape,
        input_shape=input_shape,
        strides=strides,
        dilations=dilations,
        custom_pad=custom_pad,
    )

    out_shape = []
    for r in range(num_spatial_dims):
        # only check if `input_shape` (spatial part of the input image) is symbolic, because:
        # * `input_shape` can be symbolic
        # * `pad` (aggregated from `input_shape` + ...) is symbolic only if `input_shape` is symbolic
        # * `effective_ks` (effective kernel size, determined from kernel size + dilations) cannot be symbolic
        # * strides cannot be symbolic
        if is_symbolic(input_shape[r]):
            out_shape.append(get_new_symbol())
        else:
            out_dim = 0
            if not ceil_mode:
                out_dim = math.floor((input_shape[r] + pad[r] - effective_ks[r]) / strides[r] + 1)
            else:
                out_dim = math.floor((input_shape[r] + pad[r] - effective_ks[r] + strides[r] - 1) / strides[r] + 1)
                if (out_dim - 1) * strides[r] >= input_shape[r] + pad[r]/2 and pad[r] > 0:
                    out_dim = out_dim - 1
            if out_dim <= 0:
                raise ValueError(f"spatial dimension {r} has invalid output size {out_dim}")
            out_shape.append(out_dim)
    CONV_POOLING_TYPE_INFERENCE_CACHE[cache_key] = out_shape
    return out_shape

    Args
        pad_type: string. Must be one of ('same', 'same_lower', 'valid', 'custom')

        input_shape: [iH, iW, ...]: spatial input dims (excluding channels)
            Required iff pad_type in ['same', 'same_lower']

        kernel_shape: [kH, kW, ...]: spatial kernel dims (excluding channels)

        strides: [sH, sW, ...]: spatial strides (excluding channels)
            Required iff pad_type in ['same', 'same_lower']

        dilations: [dH, dW, ...]: dilations (excluding channels)
            If not provided, defaults to [1, 1, ...], effectively no dilation.

        custom_pad: Required iff pad_type == 'custom'.
            custom_pad[2*i], custom_pad[2*i+1] are before/after custom padding
            for spatial dim i.

        ceil_mode: determines the padding and output shape.
             When ceil mode is True:
                out_dim = floor((in_dim + pad_l + pad_r - kernel_size + (stride-1)) / stride) + 1
                if (out_dim-1) * stride >= in_dim + pad_l and (pad_l > 0 or pad_r > 0):
                    out_dim = out_dim - 1
            When ceil mode is False:
                out_dim = floor((in_dim + pad_l + pad_r - kernel_size) / stride) + 1

    Returns:
        A list of spatial output sizes for each spatial dimension of kernel_shape.

    
coremltools/converters/mil/mil/ops/defs/_utils.py