text: """
  tuplevector: Treat tuples of any kind (e.g. namedtuple, NamedTuple) 
  like one dimensional vectors!
  by Connor Ferster 03/2019
  """
  
  # TODO: Explore idea of removing all checks entirely and using a decorator
  # function to wrap all vector functions (with try/excepts) to capture errors 
  # such as tuples of different lengths, elements that do not do math, or 
  # other type errors. Could vastly speed performance.
  
  from math import pi, acos, sqrt
  from numbers import Number
  from typing import Union, Any, Optional
  
  def same_shape(t1: tuple, t2: tuple) -> bool:
      """
      Returns True if t1 and t2 are the same shape. 
      False, otherwise."""
      try:
          return len(t1) == len(t2)
      except:
          return False
  
  def valid_for_arithmetic(other: Any) -> bool:
      """
      Returns True if object 'other' is valid for arithmetic operations. 
      Returns False otherwise.
      """
      try:
          sum(other) # This not a strong check but efficient for collections
          return True
      except TypeError:
          if isinstance(other, Number):
              return True
          return False
  
  def tuple_check(t1: tuple, other: Any = None) -> Optional[bool]:
      """
      Returns None. Raises error if any of the tuple validation tests fail.
      """
      is_t1_tuple = isinstance(t1, tuple)
      is_other_tuple = isinstance(other, tuple)
      is_t1_valid = valid_for_arithmetic(t1)
      is_other_valid = valid_for_arithmetic(other)
      if (is_t1_tuple and is_t1_valid and other is None):
          return True
      if (is_t1_tuple and is_t1_valid and is_other_valid and is_other_tuple):
          return (True, True)
      elif (is_t1_tuple and is_t1_valid and is_other_valid and not is_other_tuple):
          return (True, False)
      elif not is_t1_tuple: 
          raise ValueError(f"Input object, {t1}, is not a valid tuple: first arg must be tuple.")
      elif not is_t1_valid:
          raise ValueError(f"Input tuple {t1} is not valid for arithmetic operations.")
      elif other is not None:
          if is_other_tuple and not same_shape(t1, other):
              raise ValueError(f"Input tuples must be same shape, not {len(t1)} and {len(other)}.")
          if is_other_tuple and not is_other_valid:
              raise ValueError(f"Input tuple, {other}, is not valid for arithmetic operations.")
          if not is_other_tuple and not valid_for_arithmetic(other):
              raise ValueError(f"Input object, {other}, is not valid for arithmetic operations.")
  
  def dot(t1: tuple, t2: tuple) -> float:
      """
      Returns the dot product of the tuples, 't1' and 't2'.
      """
      _, t2_tup = tuple_check(t1, t2)
      if t2_tup:
          return sum([elem * t2[idx] for idx, elem in enumerate(t1)])
      else: 
          raise ValueError(f"Input tuples must be the same length. Got: {t1}, {t2}")
      
  def cross(t1: tuple, t2: tuple) -> tuple:
      """
      Returns the cross product of two 3-dimensional tuples, 't1' and 't2'.
      (Raises error if tuples are not 3-dimensional). Maintains the tuple type
      't1' (e.g. if it is a namedtuple).
      """
      t1_tup, other_tup = tuple_check(t1, t2)
      if len(t1) == len(t2) == 3:
          i = t1[1]*t2[2] - t2[1]*t1[2]
          j = -(t1[0]*t2[2] - t2[0]*t1[2])
          k = t1[0]*t2[1] - t2[0]*t1[1]
          result = (i, j, k)
          try:
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
      else:
          raise TypeError("Input tuples must be 3-dimensional. Got: {t1}, {t2}".format(t1=t1, t2=t2))
          
  def add(t1: tuple, other: Union[tuple, int, float]) -> tuple:
      """
      Returns a tuple of element-wise multiplication of 't1' and 'other'
      """
      _, other_tup = tuple_check(t1, other)
      if other_tup:
          result = tuple(elem + other[idx] for idx, elem in enumerate(t1))
          try: 
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
      else:
          result = (elem + other for elem in t1)
          try:
              print("3")
              return type(t1)(*result)
          except TypeError:
              print("4")
              return type(t1)(result)
  
  
  def subtract(t1: tuple, other: Union[tuple, int, float]) -> tuple:
      """
      Returns a tuple of element-wise multiplication of 't1' and other
      """
      _, other_tup = tuple_check(t1, other)
      if other_tup:
          result = tuple(elem - other[idx] for idx, elem in enumerate(t1))
          try: 
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
      else:
          result = (elem - other for elem in t1)
          try:
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
          
  def multiply(t1: tuple, other: Union[tuple, int, float]) -> tuple:
      """
      Returns a tuple of element-wise multiplication of 't1' and other
      """
      _, other_tup = tuple_check(t1, other)
      if other_tup:
          result = tuple(elem * other[idx] for idx, elem in enumerate(t1))
          try: 
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
      else:
          result = (elem * other for elem in t1)
          try:
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
      
  def divide(t1: tuple, other: Union[tuple, int, float], 
             ignore_zeros:bool = False) -> tuple:    
      """
      Returns a tuple of element-wise division of 't1' and 't2'.
      If 'ignore_empty' is set to False, then the division will
      ignore elements of 0/0 and will return zero for those
      elements instead.
      """
      t1_tup, other_tup = tuple_check(t1, other)
      if other_tup:
          if ignore_zeros:
              result = tuple(
                  elem / other[idx]
                  if other[idx] != 0
                  else 0 
                  for idx, elem in enumerate(t1)
                  )
          else:
              result = tuple(
                  elem / other[idx]
                  if other[idx] != 0
                  else float('nan') 
                  for idx, elem in enumerate(t1)
                  )
          try:
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
      else:
          if ignore_zeros:
              result = tuple(
                  elem / other
                  if other != 0
                  else 0 
                  for idx, elem in enumerate(t1)
              )
          else:
              result = tuple(
                  elem / other
                  if other != 0 
                  else 0 
                  for idx, elem in enumerate(t1)
              )
          try:
              return type(t1)(*result)
          except TypeError:
              return type(t1)(result)
  
  
      # acc = {}
      # if valid_for_arithmetic(other) and not isinstance(other, tuple):
      #     tuple_check(t1)
      #     for idx, val in enumerate(t1):
      #         if val == 0 and other == 0:
      #             acc.update({idx: float("nan")})
      #         elif other == 0:
      #             acc.update({idx: float("inf")})
      #         else:
      #             acc.update({idx: val/other})
      # else: 
      #     tuple_check(t1, other)
      #     for idx, val in enumerate(t1):
      #         if val == 0 and other[idx] == 0:
      #             if ignore_zeros:
      #                 acc.update({idx: 0})
      #             else:
      #                 acc.update({idx: float("nan")})
      #         elif other[idx] == 0:
      #             acc.update({idx: float("inf")})
      #         else:
      #             acc.update({idx: val/other[idx]})
      # return collapse_to_tuple(acc, type(t1))
  
  def vround(t: tuple, precision: int = 0) -> tuple:
      """
      Returns a tuple with elements rounded to 'precision'.
      """
      t_tup = tuple_check(t)
      if t_tup:
          result = tuple(round(elem, precision) for elem in t)
          try:
              return type(t)(*result)
          except TypeError:
              return type(t)(result)
  
  
  def mean(t: tuple, ignore_empty: bool = False) -> float:
      """
      Returns the average of the values in the tuple, 't'. If 'ignore_empty' 
      is True, then only the values that are not either 0 or None are averaged.
      If 'ignore_empty' is False, all values are used and None is taken as 0. 
      """
      tuple_check(t)
      count = 0
      total = 0
      for val in t:
          if ignore_empty: 
              if val == 0 or val == None:
                  continue
              else:
                  total += val
                  count += 1
          else:
              total += val
              count += 1  
      count = count or 1 # in case t is an empty vector
      return total/count
  
  def magnitude(t: tuple) -> float:
      """
      Returns the vector magnitude of the tuple, 't' using the 
      Pythagorean method.
      """
      tuple_check(t)
      mag_sqr = 0
      for val in t:
          mag_sqr += val**2
      return sqrt(mag_sqr)
      
  def normalize(t: tuple) -> tuple:
      """
      Returns the normalized unit vector of the vector tuple, 't'.
      """
      return divide(t, magnitude(t))
  
  def _clip(n: float) -> float:
      """
      Helper function to emulate numpy.clip for the specific
      use case of preventing math domain errors on the 
      acos function by "clipping" values that are > abs(1).
      e.g. _clip(1.001) == 1
           _clip(-1.5) == -1
           _clip(0.80) == 0.80
      """
      sign = n / abs(n)
      if abs(n) > 1: return 1 * sign
      else: return n
          
  
  def angle(t1: tuple, t2: tuple, degrees: bool = False) -> float:
      """
      Returns the angle between two vector tuples, 't1' and 't2',
      in rads. If 'degrees' = True, then returned in degrees.
      """
      tuple_check(t1,t2)
      rad2deg = 1.0
      if degrees: rad2deg = 180/pi
      denom = (magnitude(t1) * magnitude(t2))
      if denom == 0: return pi/4 * rad2deg
      else: return acos(_clip(dot(t1, t2) / (magnitude(t1) * magnitude(t2)))) * rad2deg
      