#(docs are generated from top-level comments, keep an eye on the formatting!)

### tensor =====================================================================

Tags
----
scripting, JS, internal, treenode, general concept

General description
-------------------
Tensors are a strange in-between of matrices and "computer"
rectangular data structures.

Tensors, unlike matrices, and like rectangular data structures,
can have an arbitrary number of dimensions (rank), although a tensor with
rank zero is just a scalar.

Tensors, like matrices and unlike many computer rectangular data structures,
must be "contiguous" i.e. have no empty spaces within its size, and "uniform",
i.e. each element must have the same shape and hence the same rank.

Also tensors have necessarily to make a distinction between row vectors,
column vectors (which have a rank of 2) and uni-dimensional vectors (rank 1).
They look very similar but they are fundamentally different.

Tensors are 1-indexed, as per general math notation, and like Fortran,
Lua, Mathematica, SASL, MATLAB, Julia, Erlang and APL.

Tensors with elements that are also tensors get promoted to a higher rank
, this is so we can represent and get the rank of a matrix correctly.
Example:
Start with a tensor of rank 1 with 2 elements (i.e. shape: 2)
if you put in both its elements another 2 tensors
of rank 1 with 2 elements (i.e. shape: 2)
then the result is a tensor of rank 2 with shape 2,2
i.e. the dimension of a tensor at all times must be
the number of nested tensors in it.
Also, all tensors must be "uniform" i.e. they must be accessed
uniformly, which means that all existing elements of a tensor
must be contiguous and have the same shape.
Implication of it all is that you can't put arbitrary
tensors inside tensors (like you would do to represent block matrices)
Rather, all tensors inside tensors must have same shape (and hence, rank)

Limitations
-----------
n.a.

Implementation info
-------------------
Tensors are implemented...

###

# Called from the "eval" module to evaluate tensor elements.
# p1 points to the tensor operand.
Eval_tensor = ->
  i = 0
  ndim = 0
  nelem = 0
  #U **a, **b

  #---------------------------------------------------------------------
  #
  #  create a new tensor for the result
  #
  #---------------------------------------------------------------------

  check_tensor_dimensions p1

  nelem = p1.tensor.nelem

  ndim = p1.tensor.ndim

  p2 = alloc_tensor(nelem)

  p2.tensor.ndim = ndim

  for i in [0...ndim]
    p2.tensor.dim[i] = p1.tensor.dim[i]

  #---------------------------------------------------------------------
  #
  #  b = Eval(a)
  #
  #---------------------------------------------------------------------

  a = p1.tensor.elem
  b = p2.tensor.elem

  check_tensor_dimensions p2

  for i in [0...nelem]
    #console.log "push/pop: pushing element a of " + i
    push(a[i])
    Eval()
    #console.log "push/pop: popping into element b of " + i
    b[i] = pop()

  check_tensor_dimensions p1
  check_tensor_dimensions p2
  #---------------------------------------------------------------------
  #
  #  push the result
  #
  #---------------------------------------------------------------------

  push(p2)

  promote_tensor()

#-----------------------------------------------------------------------------
#
#  Add tensors
#
#  Input:    Operands on stack
#
#  Output:    Result on stack
#
#-----------------------------------------------------------------------------

tensor_plus_tensor = ->
  i = 0
  ndim = 0
  nelem = 0
  #U **a, **b, **c

  save()

  p2 = pop()
  p1 = pop()

  # are the dimension lists equal?

  ndim = p1.tensor.ndim

  if (ndim != p2.tensor.ndim)
    push(symbol(NIL))
    restore()
    return

  for i in [0...ndim]
    if (p1.tensor.dim[i] != p2.tensor.dim[i])
      push(symbol(NIL))
      restore()
      return

  # create a new tensor for the result

  nelem = p1.tensor.nelem

  p3 = alloc_tensor(nelem)

  p3.tensor.ndim = ndim

  for i in [0...ndim]
    p3.tensor.dim[i] = p1.tensor.dim[i]

  # c = a + b

  a = p1.tensor.elem
  b = p2.tensor.elem
  c = p3.tensor.elem

  for i in [0...nelem]
    push(a[i])
    push(b[i])
    add()
    c[i] = pop()

  # push the result

  push(p3)

  restore()

#-----------------------------------------------------------------------------
#
#  careful not to reorder factors
#
#-----------------------------------------------------------------------------

tensor_times_scalar = ->
  i = 0
  ndim = 0
  nelem = 0
  #U **a, **b

  save()

  p2 = pop()
  p1 = pop()

  ndim = p1.tensor.ndim
  nelem = p1.tensor.nelem

  p3 = alloc_tensor(nelem)

  p3.tensor.ndim = ndim

  for i in [0...ndim]
    p3.tensor.dim[i] = p1.tensor.dim[i]

  a = p1.tensor.elem
  b = p3.tensor.elem

  for i in [0...nelem]
    push(a[i])
    push(p2)
    multiply()
    b[i] = pop()

  push(p3)
  restore()

scalar_times_tensor = ->
  i = 0
  ndim = 0
  nelem = 0
  #U **a, **b

  save()

  p2 = pop()
  p1 = pop()

  ndim = p2.tensor.ndim
  nelem = p2.tensor.nelem

  p3 = alloc_tensor(nelem)

  p3.tensor.ndim = ndim

  for i in [0...ndim]
    p3.tensor.dim[i] = p2.tensor.dim[i]

  a = p2.tensor.elem
  b = p3.tensor.elem

  for i in [0...nelem]
    push(p1)
    push(a[i])
    multiply()
    b[i] = pop()

  push(p3)

  restore()

check_tensor_dimensions = (p) ->
  if p.tensor.nelem != p.tensor.elem.length
    console.log "something wrong in tensor dimensions"
    debugger

is_square_matrix = (p) ->
  if (istensor(p) && p.tensor.ndim == 2 && p.tensor.dim[0] == p.tensor.dim[1])
    return 1
  else
    return 0

#-----------------------------------------------------------------------------
#
#  gradient of tensor
#
#-----------------------------------------------------------------------------

d_tensor_tensor = ->
  i = 0
  j = 0
  ndim = 0
  nelem = 0
  #U **a, **b, **c

  ndim = p1.tensor.ndim
  nelem = p1.tensor.nelem

  if (ndim + 1 >= MAXDIM)
    push_symbol(DERIVATIVE)
    push(p1)
    push(p2)
    list(3)
    return

  p3 = alloc_tensor(nelem * p2.tensor.nelem)

  p3.tensor.ndim = ndim + 1

  for i in [0...ndim]
    p3.tensor.dim[i] = p1.tensor.dim[i]

  p3.tensor.dim[ndim] = p2.tensor.dim[0]

  a = p1.tensor.elem
  b = p2.tensor.elem
  c = p3.tensor.elem

  for i in [0...nelem]
    for j in [0...p2.tensor.nelem]
      push(a[i])
      push(b[j])
      derivative()
      c[i * p2.tensor.nelem + j] = pop()

  push(p3)


#-----------------------------------------------------------------------------
#
#  gradient of scalar
#
#-----------------------------------------------------------------------------

d_scalar_tensor = ->
  #U **a, **b

  p3 = alloc_tensor(p2.tensor.nelem)

  p3.tensor.ndim = 1

  p3.tensor.dim[0] = p2.tensor.dim[0]

  a = p2.tensor.elem
  b = p3.tensor.elem

  for i in [0...p2.tensor.nelem]
    push(p1)
    push(a[i])
    derivative()
    b[i] = pop()

  push(p3)

#-----------------------------------------------------------------------------
#
#  Derivative of tensor
#
#-----------------------------------------------------------------------------

d_tensor_scalar = ->
  i = 0
  #U **a, **b

  p3 = alloc_tensor(p1.tensor.nelem)

  p3.tensor.ndim = p1.tensor.ndim

  for i in [0...p1.tensor.ndim]
    p3.tensor.dim[i] = p1.tensor.dim[i]

  a = p1.tensor.elem
  b = p3.tensor.elem

  for i in [0...p1.tensor.nelem]
    push(a[i])
    push(p2)
    derivative()
    b[i] = pop()

  push(p3)

compare_tensors = (p1, p2) ->

  i = 0
  if (p1.tensor.ndim < p2.tensor.ndim)
    return -1

  if (p1.tensor.ndim > p2.tensor.ndim)
    return 1

  for i in [0...p1.tensor.ndim]
    if (p1.tensor.dim[i] < p2.tensor.dim[i])
      return -1
    if (p1.tensor.dim[i] > p2.tensor.dim[i])
      return 1

  for i in [0...p1.tensor.nelem]
    if (equal(p1.tensor.elem[i], p2.tensor.elem[i]))
      continue
    if (lessp(p1.tensor.elem[i], p2.tensor.elem[i]))
      return -1
    else
      return 1

  return 0

#-----------------------------------------------------------------------------
#
#  Raise a tensor to a power
#
#  Input:    p1  tensor
#
#      p2  exponent
#
#  Output:    Result on stack
#
#-----------------------------------------------------------------------------

power_tensor = ->
  i = 0
  k = 0
  n = 0

  # first and last dims must be equal

  k = p1.tensor.ndim - 1

  if (p1.tensor.dim[0] != p1.tensor.dim[k])
    push_symbol(POWER)
    push(p1)
    push(p2)
    list(3)
    return

  push(p2)

  n = pop_integer()

  if (isNaN(n))
    push_symbol(POWER)
    push(p1)
    push(p2)
    list(3)
    return

  if (n == 0)
    if (p1.tensor.ndim != 2)
      stop("power(tensor,0) with tensor rank not equal to 2")
    n = p1.tensor.dim[0]
    p1 = alloc_tensor(n * n)
    p1.tensor.ndim = 2
    p1.tensor.dim[0] = n
    p1.tensor.dim[1] = n
    for i in [0...n]
      p1.tensor.elem[n * i + i] = one

    check_tensor_dimensions p1

    push(p1)
    return

  if (n < 0)
    n = -n
    push(p1)
    inv()
    p1 = pop()

  push(p1)

  for i in [1...n]
    push(p1)
    inner()
    if (isZeroAtomOrTensor(stack[tos - 1]))
      break

copy_tensor = ->
  i = 0

  save()

  p1 = pop()

  p2 = alloc_tensor(p1.tensor.nelem)

  p2.tensor.ndim = p1.tensor.ndim

  for i in [0...p1.tensor.ndim]
    p2.tensor.dim[i] = p1.tensor.dim[i]

  for i in [0...p1.tensor.nelem]
    p2.tensor.elem[i] = p1.tensor.elem[i]

  check_tensor_dimensions p1
  check_tensor_dimensions p2

  push(p2)

  restore()

# Tensors with elements that are also tensors get promoted to a higher rank.

promote_tensor = ->
  i = 0
  j = 0
  k = 0
  nelem = 0
  ndim = 0

  save()

  p1 = pop()

  if (!istensor(p1))
    push(p1)
    restore()
    return

  p2 = p1.tensor.elem[0]

  for i in [1...p1.tensor.nelem]
    if (!compatible(p2, p1.tensor.elem[i]))
      stop("Cannot promote tensor due to inconsistent tensor components.")

  if (!istensor(p2))
    push(p1)
    restore()
    return

  ndim = p1.tensor.ndim + p2.tensor.ndim

  if (ndim > MAXDIM)
    stop("tensor rank > " + MAXDIM)

  nelem = p1.tensor.nelem * p2.tensor.nelem

  p3 = alloc_tensor(nelem)

  p3.tensor.ndim = ndim

  for i in [0...p1.tensor.ndim]
    p3.tensor.dim[i] = p1.tensor.dim[i]

  for j in [0...p2.tensor.ndim]
    p3.tensor.dim[i + j] = p2.tensor.dim[j]

  k = 0

  for i in [0...p1.tensor.nelem]
    p2 = p1.tensor.elem[i]
    for j in [0...p2.tensor.nelem]
      p3.tensor.elem[k++] = p2.tensor.elem[j]

  check_tensor_dimensions p2
  check_tensor_dimensions p3

  push(p3)

  restore()

compatible = (p,q) ->

  if (!istensor(p) && !istensor(q))
    return 1

  if (!istensor(p) || !istensor(q))
    return 0

  if (p.tensor.ndim != q.tensor.ndim)
    return 0

  for i in [0...p.tensor.ndim]
    if (p.tensor.dim[i] != q.tensor.dim[i])
      return 0

  return 1


