# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Helper classes for tensor shape inference."""

from . import compat, dtypes
from tensorboard.compat.proto import tensor_shape_pb2


# @tf_export("Dimension")
class Dimension:
    """Represents the value of one dimension in a TensorShape."""

    def __init__(self, value):
        """Creates a new Dimension with the given value."""
        if value is None:
            self._value = None
        elif isinstance(value, dtypes.DType):
            raise TypeError("Cannot convert %s to Dimension" % value)
        else:
            self._value = int(value)
            if (
                not isinstance(value, compat.bytes_or_text_types)
                and self._value != value
            ):
                raise ValueError("Ambiguous dimension: %s" % value)
            if self._value < 0:
                raise ValueError("Dimension %d must be >= 0" % self._value)

    def __repr__(self):
        return "Dimension(%s)" % repr(self._value)

    def __str__(self):
        value = self._value
        return "?" if value is None else str(value)

    def __eq__(self, other):
        """Returns true if `other` has the same known value as this
        Dimension."""
        try:
            other = as_dimension(other)
        except (TypeError, ValueError):
            return NotImplemented
        if self._value is None or other.value is None:
            return None
        return self._value == other.value

    def __ne__(self, other):
        """Returns true if `other` has a different known value from `self`."""
        try:
            other = as_dimension(other)
        except (TypeError, ValueError):
            return NotImplemented
        if self._value is None or other.value is None:
            return None
        return self._value != other.value

    def __int__(self):
        return self._value

    # This is needed for Windows.
    # See https://github.com/tensorflow/tensorflow/pull/9780
    def __long__(self):
        return self._value

    def __index__(self):
        # Allow use in Python 3 range
        return self._value

    @property
    def value(self):
        """The value of this dimension, or None if it is unknown."""
        return self._value

    def is_convertible_with(self, other):
        """Returns true if `other` is convertible with this Dimension.

        Two known Dimensions are convertible if they have the same value.
        An unknown Dimension is convertible with all other Dimensions.

        Args:
          other: Another Dimension.

        Returns:
          True if this Dimension and `other` are convertible.
        """
        other = as_dimension(other)
        return (
            self._value is None
            or other.value is None
            or self._value == other.value
        )

    def assert_is_convertible_with(self, other):
        """Raises an exception if `other` is not convertible with this
        Dimension.

        Args:
          other: Another Dimension.

        Raises:
          ValueError: If `self` and `other` are not convertible (see
            is_convertible_with).
        """
        if not self.is_convertible_with(other):
            raise ValueError(
                "Dimensions %s and %s are not convertible" % (self, other)
            )

    def merge_with(self, other):
        """Returns a Dimension that combines the information in `self` and
        `other`.

        Dimensions are combined as follows:

        ```python
        tf.Dimension(n)   .merge_with(tf.Dimension(n))    == tf.Dimension(n)
        tf.Dimension(n)   .merge_with(tf.Dimension(None)) == tf.Dimension(n)
        tf.Dimension(None).merge_with(tf.Dimension(n))    == tf.Dimension(n)
        tf.Dimension(None).merge_with(tf.Dimension(None)) == tf.Dimension(None)
        tf.Dimension(n)   .merge_with(tf.Dimension(m))  # raises ValueError for n != m
        ```

        Args:
          other: Another Dimension.

        Returns:
          A Dimension containing the combined information of `self` and
          `other`.

        Raises:
          ValueError: If `self` and `other` are not convertible (see
            is_convertible_with).
        """
        other = as_dimension(other)
        self.assert_is_convertible_with(other)
        if self._value is None:
            return Dimension(other.value)
        else:
            return Dimension(self._value)

    def __add__(self, other):
        """Returns the sum of `self` and `other`.

        Dimensions are summed as follows:

        ```python
        tf.Dimension(m)    + tf.Dimension(n)    == tf.Dimension(m + n)
        tf.Dimension(m)    + tf.Dimension(None) == tf.Dimension(None)
        tf.Dimension(None) + tf.Dimension(n)    == tf.Dimension(None)
        tf.Dimension(None) + tf.Dimension(None) == tf.Dimension(None)
        ```

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is the sum of `self` and `other`.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(self._value + other.value)

    def __radd__(self, other):
        """Returns the sum of `other` and `self`.

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is the sum of `self` and `other`.
        """
        return self + other

    def __sub__(self, other):
        """Returns the subtraction of `other` from `self`.

        Dimensions are subtracted as follows:

        ```python
        tf.Dimension(m)    - tf.Dimension(n)    == tf.Dimension(m - n)
        tf.Dimension(m)    - tf.Dimension(None) == tf.Dimension(None)
        tf.Dimension(None) - tf.Dimension(n)    == tf.Dimension(None)
        tf.Dimension(None) - tf.Dimension(None) == tf.Dimension(None)
        ```

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is the subtraction of `other` from `self`.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(self._value - other.value)

    def __rsub__(self, other):
        """Returns the subtraction of `self` from `other`.

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is the subtraction of `self` from `other`.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(other.value - self._value)

    def __mul__(self, other):
        """Returns the product of `self` and `other`.

        Dimensions are summed as follows:

        ```python
        tf.Dimension(m)    * tf.Dimension(n)    == tf.Dimension(m * n)
        tf.Dimension(m)    * tf.Dimension(None) == tf.Dimension(None)
        tf.Dimension(None) * tf.Dimension(n)    == tf.Dimension(None)
        tf.Dimension(None) * tf.Dimension(None) == tf.Dimension(None)
        ```

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is the product of `self` and `other`.
        """
        try:
            other = as_dimension(other)
        except (TypeError, ValueError):
            return NotImplemented

        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(self._value * other.value)

    def __rmul__(self, other):
        """Returns the product of `self` and `other`.

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is the product of `self` and `other`.
        """
        return self * other

    def __floordiv__(self, other):
        """Returns the quotient of `self` and `other` rounded down.

        Dimensions are divided as follows:

        ```python
        tf.Dimension(m)    // tf.Dimension(n)    == tf.Dimension(m // n)
        tf.Dimension(m)    // tf.Dimension(None) == tf.Dimension(None)
        tf.Dimension(None) // tf.Dimension(n)    == tf.Dimension(None)
        tf.Dimension(None) // tf.Dimension(None) == tf.Dimension(None)
        ```

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A `Dimension` whose value is the integer quotient of `self` and `other`.
        """
        try:
            other = as_dimension(other)
        except (TypeError, ValueError):
            return NotImplemented
        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(self._value // other.value)

    def __rfloordiv__(self, other):
        """Returns the quotient of `other` and `self` rounded down.

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A `Dimension` whose value is the integer quotient of `self` and `other`.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(other.value // self._value)

    def __div__(self, other):
        """DEPRECATED: Use `__floordiv__` via `x // y` instead.

        This function exists only for backwards convertibility purposes; new code
        should use `__floordiv__` via the syntax `x // y`.  Using `x // y`
        communicates clearly that the result rounds down, and is forward convertible
        to Python 3.

        Args:
          other: Another `Dimension`.

        Returns:
          A `Dimension` whose value is the integer quotient of `self` and `other`.
        """
        return self // other

    def __mod__(self, other):
        """Returns `self` modulo `other`.

        Dimension moduli are computed as follows:

        ```python
        tf.Dimension(m)    % tf.Dimension(n)    == tf.Dimension(m % n)
        tf.Dimension(m)    % tf.Dimension(None) == tf.Dimension(None)
        tf.Dimension(None) % tf.Dimension(n)    == tf.Dimension(None)
        tf.Dimension(None) % tf.Dimension(None) == tf.Dimension(None)
        ```

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is `self` modulo `other`.
        """
        try:
            other = as_dimension(other)
        except (TypeError, ValueError):
            return NotImplemented
        if self._value is None or other.value is None:
            return Dimension(None)
        else:
            return Dimension(self._value % other.value)

    def __rmod__(self, other):
        """Returns `other` modulo `self`.

        Args:
          other: Another Dimension, or a value accepted by `as_dimension`.

        Returns:
          A Dimension whose value is `other` modulo `self`.
        """
        try:
            other = as_dimension(other)
        except (TypeError, ValueError):
            return NotImplemented
        return other % self

    def __lt__(self, other):
        """Returns True if `self` is known to be less than `other`.

        Dimensions are compared as follows:

        ```python
        (tf.Dimension(m)    < tf.Dimension(n))    == (m < n)
        (tf.Dimension(m)    < tf.Dimension(None)) == None
        (tf.Dimension(None) < tf.Dimension(n))    == None
        (tf.Dimension(None) < tf.Dimension(None)) == None
        ```

        Args:
          other: Another Dimension.

        Returns:
          The value of `self.value < other.value` if both are known, otherwise
          None.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return None
        else:
            return self._value < other.value

    def __le__(self, other):
        """Returns True if `self` is known to be less than or equal to `other`.

        Dimensions are compared as follows:

        ```python
        (tf.Dimension(m)    <= tf.Dimension(n))    == (m <= n)
        (tf.Dimension(m)    <= tf.Dimension(None)) == None
        (tf.Dimension(None) <= tf.Dimension(n))    == None
        (tf.Dimension(None) <= tf.Dimension(None)) == None
        ```

        Args:
          other: Another Dimension.

        Returns:
          The value of `self.value <= other.value` if both are known, otherwise
          None.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return None
        else:
            return self._value <= other.value

    def __gt__(self, other):
        """Returns True if `self` is known to be greater than `other`.

        Dimensions are compared as follows:

        ```python
        (tf.Dimension(m)    > tf.Dimension(n))    == (m > n)
        (tf.Dimension(m)    > tf.Dimension(None)) == None
        (tf.Dimension(None) > tf.Dimension(n))    == None
        (tf.Dimension(None) > tf.Dimension(None)) == None
        ```

        Args:
          other: Another Dimension.

        Returns:
          The value of `self.value > other.value` if both are known, otherwise
          None.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return None
        else:
            return self._value > other.value

    def __ge__(self, other):
        """Returns True if `self` is known to be greater than or equal to
        `other`.

        Dimensions are compared as follows:

        ```python
        (tf.Dimension(m)    >= tf.Dimension(n))    == (m >= n)
        (tf.Dimension(m)    >= tf.Dimension(None)) == None
        (tf.Dimension(None) >= tf.Dimension(n))    == None
        (tf.Dimension(None) >= tf.Dimension(None)) == None
        ```

        Args:
          other: Another Dimension.

        Returns:
          The value of `self.value >= other.value` if both are known, otherwise
          None.
        """
        other = as_dimension(other)
        if self._value is None or other.value is None:
            return None
        else:
            return self._value >= other.value

    def __reduce__(self):
        return Dimension, (self._value,)


def as_dimension(value):
    """Converts the given value to a Dimension.

    A Dimension input will be returned unmodified.
    An input of `None` will be converted to an unknown Dimension.
    An integer input will be converted to a Dimension with that value.

    Args:
      value: The value to be converted.

    Returns:
      A Dimension corresponding to the given value.
    """
    if isinstance(value, Dimension):
        return value
    else:
        return Dimension(value)


# @tf_export("TensorShape")
class TensorShape:
    """Represents the shape of a `Tensor`.

    A `TensorShape` represents a possibly-partial shape specification for a
    `Tensor`. It may be one of the following:

    * *Fully-known shape:* has a known number of dimensions and a known size
      for each dimension. e.g. `TensorShape([16, 256])`
    * *Partially-known shape:* has a known number of dimensions, and an unknown
      size for one or more dimension. e.g. `TensorShape([None, 256])`
    * *Unknown shape:* has an unknown number of dimensions, and an unknown
      size in all dimensions. e.g. `TensorShape(None)`

    If a tensor is produced by an operation of type `"Foo"`, its shape
    may be inferred if there is a registered shape function for
    `"Foo"`. See @{$adding_an_op#shape-functions-in-c$`Shape functions in C++`}
    for details of shape functions and how to register them. Alternatively,
    the shape may be set explicitly using @{tf.Tensor.set_shape}.
    """

    def __init__(self, dims):
        """Creates a new TensorShape with the given dimensions.

        Args:
          dims: A list of Dimensions, or None if the shape is unspecified.
            DEPRECATED: A single integer is treated as a singleton list.

        Raises:
          TypeError: If dims cannot be converted to a list of dimensions.
        """
        # TODO(irving): Eliminate the single integer special case.
        if dims is None:
            self._dims = None
        elif isinstance(dims, compat.bytes_or_text_types):
            raise TypeError(
                "A string has ambiguous TensorShape, please wrap in a "
                "list or convert to an int: %s" % dims
            )
        elif isinstance(dims, tensor_shape_pb2.TensorShapeProto):
            if dims.unknown_rank:
                self._dims = None
            else:
                self._dims = [
                    # Protos store variable-size dimensions as -1
                    as_dimension(dim.size if dim.size != -1 else None)
                    for dim in dims.dim
                ]
        elif isinstance(dims, TensorShape):
            self._dims = dims.dims
        else:
            try:
                dims_iter = iter(dims)
            except TypeError:
                # Treat as a singleton dimension
                self._dims = [as_dimension(dims)]
            else:
                # Got a list of dimensions
                self._dims = [as_dimension(d) for d in dims_iter]
        self._ndims = None

    def __repr__(self):
        return "TensorShape(%r)" % self._dims

    def __str__(self):
        if self.ndims is None:
            return "<unknown>"
        elif self.ndims == 1:
            return "(%s,)" % self._dims[0]
        else:
            return "(%s)" % ", ".join(str(d) for d in self._dims)

    @property
    def dims(self):
        """Returns a list of Dimensions, or None if the shape is
        unspecified."""
        return self._dims

    @dims.setter
    def dims(self, dims):
        self._dims = dims
        self._ndims = None

    @property
    def ndims(self):
        """Returns the rank of this shape, or None if it is unspecified."""
        if self._dims is None:
            return None
        else:
            if self._ndims is None:
                self._ndims = len(self._dims)
            return self._ndims

    def __len__(self):
        """Returns the rank of this shape, or raises ValueError if
        unspecified."""
        if self._dims is None:
            raise ValueError(
                "Cannot take the length of Shape with unknown rank."
            )
        return self.ndims

    def __bool__(self):
        """Returns True if this shape contains non-zero information."""
        return self._dims is not None

    # Python 3 wants __bool__, Python 2.7 wants __nonzero__
    __nonzero__ = __bool__

    def __iter__(self):
        """Returns `self.dims` if the rank is known, otherwise raises
        ValueError."""
        if self._dims is None:
            raise ValueError("Cannot iterate over a shape with unknown rank.")
        else:
            return iter(self._dims)

    def __getitem__(self, key):
        """Returns the value of a dimension or a shape, depending on the key.

        Args:
          key: If `key` is an integer, returns the dimension at that index;
            otherwise if `key` is a slice, returns a TensorShape whose
            dimensions are those selected by the slice from `self`.

        Returns:
          A dimension if `key` is an integer, or a `TensorShape` if `key` is a
          slice.

        Raises:
          ValueError: If `key` is a slice, and any of its elements are negative, or
            if `self` is completely unknown and the step is set.
        """
        if self._dims is not None:
            if isinstance(key, slice):
                return TensorShape(self._dims[key])
            else:
                return self._dims[key]
        else:
            if isinstance(key, slice):
                start = key.start if key.start is not None else 0
                stop = key.stop

                if key.step is not None:
                    # TODO(mrry): Handle these maybe.
                    raise ValueError("Steps are not yet handled")
                if stop is None:
                    # NOTE(mrry): This implies that TensorShape(None) is convertible with
                    # TensorShape(None)[1:], which is obviously not true. It would be
                    # possible to track the number of dimensions symbolically,
                    # and perhaps we should do that.
                    return unknown_shape()
                elif start < 0 or stop < 0:
                    # TODO(mrry): Handle this better, as it will be useful for handling
                    # suffixes of otherwise unknown shapes.
                    return unknown_shape()
                else:
                    return unknown_shape(ndims=stop - start)
            else:
                return Dimension(None)

    def num_elements(self):
        """Returns the total number of elements, or none for incomplete
        shapes."""
        if self.is_fully_defined():
            size = 1
            for dim in self._dims:
                size *= dim.value
            return size
        else:
            return None

    def merge_with(self, other):
        """Returns a `TensorShape` combining the information in `self` and
        `other`.

        The dimensions in `self` and `other` are merged elementwise,
        according to the rules defined for `Dimension.merge_with()`.

        Args:
          other: Another `TensorShape`.

        Returns:
          A `TensorShape` containing the combined information of `self` and
          `other`.

        Raises:
          ValueError: If `self` and `other` are not convertible.
        """
        other = as_shape(other)
        if self._dims is None:
            return other
        else:
            try:
                self.assert_same_rank(other)
                new_dims = []
                for i, dim in enumerate(self._dims):
                    new_dims.append(dim.merge_with(other[i]))
                return TensorShape(new_dims)
            except ValueError:
                raise ValueError(
                    "Shapes %s and %s are not convertible" % (self, other)
                )

    def concatenate(self, other):
        """Returns the concatenation of the dimension in `self` and `other`.

        *N.B.* If either `self` or `other` is completely unknown,
        concatenation will discard information about the other shape. In
        future, we might support concatenation that preserves this
        information for use with slicing.

        Args:
          other: Another `TensorShape`.

        Returns:
          A `TensorShape` whose dimensions are the concatenation of the
          dimensions in `self` and `other`.
        """
        # TODO(mrry): Handle the case where we concatenate a known shape with a
        # completely unknown shape, so that we can use the partial information.
        other = as_shape(other)
        if self._dims is None or other.dims is None:
            return unknown_shape()
        else:
            return TensorShape(self._dims + other.dims)

    def assert_same_rank(self, other):
        """Raises an exception if `self` and `other` do not have convertible
        ranks.

        Args:
          other: Another `TensorShape`.

        Raises:
          ValueError: If `self` and `other` do not represent shapes with the
            same rank.
        """
        other = as_shape(other)
        if self.ndims is not None and other.ndims is not None:
            if self.ndims != other.ndims:
                raise ValueError(
                    "Shapes %s and %s must have the same rank" % (self, other)
                )

    def assert_has_rank(self, rank):
        """Raises an exception if `self` is not convertible with the given
        `rank`.

        Args:
          rank: An integer.

        Raises:
          ValueError: If `self` does not represent a shape with the given `rank`.
        """
        if self.ndims not in (None, rank):
            raise ValueError("Shape %s must have rank %d" % (self, rank))

    def with_rank(self, rank):
        """Returns a shape based on `self` with the given rank.

        This method promotes a completely unknown shape to one with a
        known rank.

        Args:
          rank: An integer.

        Returns:
          A shape that is at least as specific as `self` with the given rank.

        Raises:
          ValueError: If `self` does not represent a shape with the given `rank`.
        """
        try:
            return self.merge_with(unknown_shape(ndims=rank))
        except ValueError:
            raise ValueError("Shape %s must have rank %d" % (self, rank))

    def with_rank_at_least(self, rank):
        """Returns a shape based on `self` with at least the given rank.

        Args:
          rank: An integer.

        Returns:
          A shape that is at least as specific as `self` with at least the given
          rank.

        Raises:
          ValueError: If `self` does not represent a shape with at least the given
            `rank`.
        """
        if self.ndims is not None and self.ndims < rank:
            raise ValueError(
                "Shape %s must have rank at least %d" % (self, rank)
            )
        else:
            return self

    def with_rank_at_most(self, rank):
        """Returns a shape based on `self` with at most the given rank.

        Args:
          rank: An integer.

        Returns:
          A shape that is at least as specific as `self` with at most the given
          rank.

        Raises:
          ValueError: If `self` does not represent a shape with at most the given
            `rank`.
        """
        if self.ndims is not None and self.ndims > rank:
            raise ValueError(
                "Shape %s must have rank at most %d" % (self, rank)
            )
        else:
            return self

    def is_convertible_with(self, other):
        """Returns True iff `self` is convertible with `other`.

        Two possibly-partially-defined shapes are convertible if there
        exists a fully-defined shape that both shapes can represent. Thus,
        convertibility allows the shape inference code to reason about
        partially-defined shapes. For example:

        * TensorShape(None) is convertible with all shapes.

        * TensorShape([None, None]) is convertible with all two-dimensional
          shapes, such as TensorShape([32, 784]), and also TensorShape(None). It is
          not convertible with, for example, TensorShape([None]) or
          TensorShape([None, None, None]).

        * TensorShape([32, None]) is convertible with all two-dimensional shapes
          with size 32 in the 0th dimension, and also TensorShape([None, None])
          and TensorShape(None). It is not convertible with, for example,
          TensorShape([32]), TensorShape([32, None, 1]) or TensorShape([64, None]).

        * TensorShape([32, 784]) is convertible with itself, and also
          TensorShape([32, None]), TensorShape([None, 784]), TensorShape([None,
          None]) and TensorShape(None). It is not convertible with, for example,
          TensorShape([32, 1, 784]) or TensorShape([None]).

        The convertibility relation is reflexive and symmetric, but not
        transitive. For example, TensorShape([32, 784]) is convertible with
        TensorShape(None), and TensorShape(None) is convertible with
        TensorShape([4, 4]), but TensorShape([32, 784]) is not convertible with
        TensorShape([4, 4]).

        Args:
          other: Another TensorShape.

        Returns:
          True iff `self` is convertible with `other`.
        """
        other = as_shape(other)
        if self._dims is not None and other.dims is not None:
            if self.ndims != other.ndims:
                return False
            for x_dim, y_dim in zip(self._dims, other.dims):
                if not x_dim.is_convertible_with(y_dim):
                    return False
        return True

    def assert_is_convertible_with(self, other):
        """Raises exception if `self` and `other` do not represent the same
        shape.

        This method can be used to assert that there exists a shape that both
        `self` and `other` represent.

        Args:
          other: Another TensorShape.

        Raises:
          ValueError: If `self` and `other` do not represent the same shape.
        """
        if not self.is_convertible_with(other):
            raise ValueError(
                "Shapes %s and %s are inconvertible" % (self, other)
            )

    def most_specific_convertible_shape(self, other):
        """Returns the most specific TensorShape convertible with `self` and
        `other`.

        * TensorShape([None, 1]) is the most specific TensorShape convertible with
          both TensorShape([2, 1]) and TensorShape([5, 1]). Note that
          TensorShape(None) is also convertible with above mentioned TensorShapes.

        * TensorShape([1, 2, 3]) is the most specific TensorShape convertible with
          both TensorShape([1, 2, 3]) and TensorShape([1, 2, 3]). There are more
          less specific TensorShapes convertible with above mentioned TensorShapes,
          e.g. TensorShape([1, 2, None]), TensorShape(None).

        Args:
          other: Another `TensorShape`.

        Returns:
          A `TensorShape` which is the most specific convertible shape of `self`
          and `other`.
        """

        other = as_shape(other)
        if (
            self._dims is None
            or other.dims is None
            or self.ndims != other.ndims
        ):
            return unknown_shape()

        dims = [(Dimension(None))] * self.ndims
        for i, (d1, d2) in enumerate(zip(self._dims, other.dims)):
            if d1 is not None and d2 is not None and d1 == d2:
                dims[i] = d1
        return TensorShape(dims)

    def is_fully_defined(self):
        """Returns True iff `self` is fully defined in every dimension."""
        return self._dims is not None and all(
            dim.value is not None for dim in self._dims
        )

    def assert_is_fully_defined(self):
        """Raises an exception if `self` is not fully defined in every
        dimension.

        Raises:
          ValueError: If `self` does not have a known value for every dimension.
        """
        if not self.is_fully_defined():
            raise ValueError("Shape %s is not fully defined" % self)

    def as_list(self):
        """Returns a list of integers or `None` for each dimension.

        Returns:
          A list of integers or `None` for each dimension.

        Raises:
          ValueError: If `self` is an unknown shape with an unknown rank.
        """
        if self._dims is None:
            raise ValueError(
                "as_list() is not defined on an unknown TensorShape."
            )
        return [dim.value for dim in self._dims]

    def as_proto(self):
        """Returns this shape as a `TensorShapeProto`."""
        if self._dims is None:
            return tensor_shape_pb2.TensorShapeProto(unknown_rank=True)
        else:
            return tensor_shape_pb2.TensorShapeProto(
                dim=[
                    tensor_shape_pb2.TensorShapeProto.Dim(
                        size=-1 if d.value is None else d.value
                    )
                    for d in self._dims
                ]
            )

    def __eq__(self, other):
        """Returns True if `self` is equivalent to `other`."""
        try:
            other = as_shape(other)
        except TypeError:
            return NotImplemented
        return self._dims == other.dims

    def __ne__(self, other):
        """Returns True if `self` is known to be different from `other`."""
        try:
            other = as_shape(other)
        except TypeError:
            return NotImplemented
        if self.ndims is None or other.ndims is None:
            raise ValueError(
                "The inequality of unknown TensorShapes is undefined."
            )
        if self.ndims != other.ndims:
            return True
        return self._dims != other.dims

    def __reduce__(self):
        return TensorShape, (self._dims,)


def as_shape(shape):
    """Converts the given object to a TensorShape."""
    if isinstance(shape, TensorShape):
        return shape
    else:
        return TensorShape(shape)


def unknown_shape(ndims=None):
    """Returns an unknown TensorShape, optionally with a known rank.

    Args:
      ndims: (Optional) If specified, the number of dimensions in the shape.

    Returns:
      An unknown TensorShape.
    """
    if ndims is None:
        return TensorShape(None)
    else:
        return TensorShape([Dimension(None)] * ndims)


_SCALAR_SHAPE = TensorShape([])


def scalar():
    """Returns a shape representing a scalar."""
    return _SCALAR_SHAPE


def vector(length):
    """Returns a shape representing a vector.

    Args:
      length: The length of the vector, which may be None if unknown.

    Returns:
      A TensorShape representing a vector of the given length.
    """
    return TensorShape([length])


def matrix(rows, cols):
    """Returns a shape representing a matrix.

    Args:
      rows: The number of rows in the matrix, which may be None if unknown.
      cols: The number of columns in the matrix, which may be None if unknown.

    Returns:
      A TensorShape representing a matrix of the given size.
    """
    return TensorShape([rows, cols])
