def fft_canonicalize_shapes_dims(
    input_data: Var, shapes: Optional[Var], dims: Optional[Var], c2r: bool = False
) -> Tuple[Tuple[int], Tuple[int]]:
    """
    Canonicalize shapes and dims for N-D FFT (based on PyTorch's fftn documentation):
    - shapes: Signal size in the transformed dimensions. If given, each dimension dims[i] will
              either be zero-padded or trimmed to the length s[i] before computing the FFT. If a
              length -1 is specified, no padding is done in that dimension.
              Default: s = [input.size(d) for d in dims]
    - dims: Dimensions to be transformed. Default: all dimensions, or the last len(s) dimensions if
            s is given.
    - c2r: Use for "complex to real", such as irfftn, which takes complex and output real data.
    """
    if shapes is not None:
        shapes = shapes.val
        if isinstance(shapes, np.integer):
            shapes = (shapes,)
    if dims is not None:
        dims = dims.val
        if isinstance(dims, np.integer):
            dims = (dims,)

    # Input validation.
    input_rank = input_data.rank
    if dims is not None:
        for dim in dims:
            if dim < -input_rank or dim >= input_rank:
                raise ValueError(f"Invalid dim {dim} in `dims`.")
    if shapes is not None:
        for shape in shapes:
            if shape <= 0:
                raise ValueError(f"Invalid shape {shape} in `shapes`.")

    # Determine if the last dim specified in dims need to be expanded. For IRFFTN, the input is
    # interpreted as a one-sided Hermitian signal in the Fourier domain, as produced by rfftn(), so
    # we need to expand the dim back to the full matrix (with conjugate part not pruned).
    last_dim_expand: bool = shapes is None and c2r

    if shapes is not None:
        if dims is None:
            # Has shape, no dim.
            # Default is last len(s) dimensions.
            dims = tuple(range(input_rank - len(shapes), input_rank))
        else:
            # Has shape, has dim.
            if len(shapes) != len(dims):
                raise ValueError(
                    "shapes and dims must have the same number of elements."
                )
        shapes = tuple(
            shape if shape != -1 else input_data.shape[dim]
            for (shape, dim) in zip(shapes, dims)
        )
    elif dims is None:
        # No shape, no dim.
        dims = tuple(range(input_rank))
        shapes = tuple(input_data.shape)
    else:
        # No shape, has dim.
        shapes = tuple(input_data.shape[dim] for dim in dims)

    # In RFFTN, the output is trimmed (because FFT of real-value input is Hermitian-symmetric, the
    # conjugate part is removed) to ``original_dim // 2 + 1``, so here we do the reverse
    # ``2 * (trimmed_dim - 1)`` to restore the original shape.
    if last_dim_expand:
        target_last_dim_shape = 2 * (input_data.shape[dims[-1]] - 1)
        shapes = shapes[:-1] + (target_last_dim_shape,)

    if len(shapes) != len(dims):
        raise ValueError(
            f"shape ({len(shapes)}) and dim ({len(dims)}) should have same number of elements."
        )

    return shapes, dims

    Canonicalize shapes and dims for N-D FFT (based on PyTorch's fftn documentation):
    - shapes: Signal size in the transformed dimensions. If given, each dimension dims[i] will
              either be zero-padded or trimmed to the length s[i] before computing the FFT. If a
              length -1 is specified, no padding is done in that dimension.
              Default: s = [input.size(d) for d in dims]
    - dims: Dimensions to be transformed. Default: all dimensions, or the last len(s) dimensions if
            s is given.
    - c2r: Use for "complex to real", such as irfftn, which takes complex and output real data.
    
coremltools/converters/mil/mil/ops/defs/complex_dialect_ops.py