algebrite
Version:
Computer Algebra System in Coffeescript
450 lines (388 loc) • 10.9 kB
text/coffeescript
# the inner (or dot) operator gives products of vectors,
# matrices, and tensors.
#
# Note that for Algebrite, the elements of a vector/matrix
# can only be scalars. This allows for example to flesh out
# matrix multiplication using the usual multiplication.
# So for example block-representations are not allowed.
#
# There is an aweful lot of confusion between sw packages on
# what dot and inner do.
#
# First off, the "dot" operator is different from the
# mathematical notion of dot product, which can be
# slightly confusing.
#
# The mathematical notion of dot product is here:
# http://mathworld.wolfram.com/DotProduct.html
#
# However, "dot" does that and a bunch of other things,
# i.e. in Algebrite
# dot/inner does what the dot of Mathematica does, i.e.:
#
# scalar product of vectors:
#
# inner((a, b, c), (x, y, z))
# > a x + b y + c z
#
# products of matrices and vectors:
#
# inner(((a, b), (c,d)), (x, y))
# > (a x + b y,c x + d y)
#
# inner((x, y), ((a, b), (c,d)))
# > (a x + c y,b x + d y)
#
# inner((x, y), ((a, b), (c,d)), (r, s))
# > a r x + b s x + c r y + d s y
#
# matrix product:
#
# inner(((a,b),(c,d)),((r,s),(t,u)))
# > ((a r + b t,a s + b u),(c r + d t,c s + d u))
#
# the "dot/inner" operator is associative and
# distributive but not commutative.
#
# In Mathematica, Inner is a generalisation of Dot where
# the user can specify the multiplication and the addition
# operators.
# But here in Algebrite they do the same thing.
#
# https://reference.wolfram.com/language/ref/Dot.html
# https://reference.wolfram.com/language/ref/Inner.html
#
# http://uk.mathworks.com/help/matlab/ref/dot.html
# http://uk.mathworks.com/help/matlab/ref/mtimes.html
Eval_inner = ->
# if there are more than two arguments then
# reduce it to a more standard version
# of two arguments, which means we need to
# transform the arguments into a tree of
# inner products e.g.
# inner(a,b,c) becomes inner(a,inner(b,c))
# this is so we can get to a standard binary-tree
# version that is simpler to manipulate.
theArguments = []
theArguments.push car(cdr(p1))
secondArgument = car(cdr(cdr(p1)))
if secondArgument == symbol(NIL)
stop("pattern needs at least a template and a transformed version")
moretheArguments = cdr(cdr(p1))
while moretheArguments != symbol(NIL)
theArguments.push car(moretheArguments)
moretheArguments = cdr(moretheArguments)
# make it so e.g. inner(a,b,c) becomes inner(a,inner(b,c))
if theArguments.length > 2
push_symbol(INNER)
push theArguments[theArguments.length-2]
push theArguments[theArguments.length-1]
list(3)
for i in [2...theArguments.length]
push_symbol(INNER)
swap()
push theArguments[theArguments.length-i-1]
swap()
list(3)
p1 = pop()
Eval_inner()
return
# TODO we have to take a look at the whole
# sequence of operands and make simplifications
# on that...
operands = []
get_innerprod_factors(p1, operands)
#console.log "printing operands --------"
#for i in [0...operands.length]
# console.log "operand " + i + " : " + operands[i]
refinedOperands = []
# removing all identity matrices
for i in [0...operands.length]
if operands[i] == symbol(SYMBOL_IDENTITY_MATRIX)
continue
else refinedOperands.push operands[i]
operands = refinedOperands
refinedOperands = []
if operands.length > 1
# removing all consecutive couples of inverses
shift = 0
for i in [0...operands.length]
#console.log "comparing if " + operands[i+shift] + " and " + operands[i+shift+1] + " are inverses of each other"
if (i+shift+1) <= (operands.length - 1)
push operands[i+shift]
Eval()
inv()
push operands[i+shift+1]
Eval()
subtract()
difference = pop()
#console.log "result: " + difference
if (iszero(difference))
shift+=1
else
refinedOperands.push operands[i+shift]
else
break
#console.log "i: " + i + " shift: " + shift + " operands.length: " + operands.length
if i+shift == operands.length - 2
#console.log "adding last operand 2 "
refinedOperands.push operands[operands.length-1]
if i+shift >= operands.length - 1
break
operands = refinedOperands
#console.log "refined operands --------"
#for i in [0...refinedOperands.length]
# console.log "refined operand " + i + " : " + refinedOperands[i]
#console.log "stack[tos-1]: " + stack[tos-1]
# now rebuild the arguments, just using the
# refined operands
push symbol(INNER)
#console.log "rebuilding the argument ----"
if operands.length > 0
for i in [0...operands.length]
#console.log "pushing " + operands[i]
push operands[i]
else
pop()
push symbol(SYMBOL_IDENTITY_MATRIX)
return
#console.log "list(operands.length): " + (operands.length+1)
list(operands.length + 1)
p1 = pop()
p1 = cdr(p1)
push(car(p1))
Eval()
p1 = cdr(p1)
while (iscons(p1))
push(car(p1))
Eval()
inner()
p1 = cdr(p1)
# inner definition
inner = ->
save()
p2 = pop()
p1 = pop()
# more in general, when a and b are scalars,
# inner(a*M1, b*M2) is equal to
# a*b*inner(M1,M2), but of course we can only
# "bring out" in a and b the scalars, because
# it's the only commutative part.
# that's going to be trickier to do in general
# but let's start with just the signs.
if isnegativeterm(p2) and isnegativeterm(p1)
push p2
negate()
p2 = pop()
push p1
negate()
p1 = pop()
# since inner is associative,
# put it in a canonical form i.e.
# inner(inner(a,b),c) ->
# inner(a,inner(b,c))
# so that we can recognise when they
# are equal.
if isinnerordot(p1)
arg1 = car(cdr(p1)) #a
arg2 = car(cdr(cdr(p1))) #b
arg3 = p2
p1 = arg1
push arg2
push arg3
inner()
p2 = pop()
# Check if one of the operands is the identity matrix
# we could maybe use Eval_testeq here but
# this seems to suffice?
if p1 == symbol(SYMBOL_IDENTITY_MATRIX)
push p2
restore()
return
else if p2 == symbol(SYMBOL_IDENTITY_MATRIX)
push p1
restore()
return
if (istensor(p1) && istensor(p2))
inner_f()
else
# simple check if the two elements are one the
# inv of the other. If they are, the answer is
# the identity matrix
push p1
push p2
inv()
subtract()
subtractionResult = pop()
if (iszero(subtractionResult))
push_symbol(SYMBOL_IDENTITY_MATRIX)
restore()
return
# if either operand is a sum then distribute
# (if we are in expanding mode)
if (expanding && isadd(p1))
p1 = cdr(p1)
push(zero)
while (iscons(p1))
push(car(p1))
push(p2)
inner()
add()
p1 = cdr(p1)
restore()
return
if (expanding && isadd(p2))
p2 = cdr(p2)
push(zero)
while (iscons(p2))
push(p1)
push(car(p2))
inner()
add()
p2 = cdr(p2)
restore()
return
push(p1)
push(p2)
# there are 8 remaining cases here, since each of the
# two arguments can only be a scalar/tensor/unknown
# and the tensor - tensor case was caught
# upper in the code
if (istensor(p1) and isnum(p2))
# one case covered by this branch:
# tensor - scalar
tensor_times_scalar()
else if (isnum(p1) and istensor(p2))
# one case covered by this branch:
# scalar - tensor
scalar_times_tensor()
else
if (isnum(p1) or isnum(p2))
# three cases covered by this branch:
# unknown - scalar
# scalar - unknown
# scalar - scalar
# in these cases a normal multiplication
# will be OK
multiply()
else
# three cases covered by this branch:
# unknown - unknown
# unknown - tensor
# tensor - unknown
# in this case we can't use normal
# multiplication.
pop()
pop()
push_symbol(INNER)
push(p1)
push(p2)
list(3)
restore()
return
restore()
# inner product of tensors p1 and p2
inner_f = ->
i = 0
n = p1.tensor.dim[p1.tensor.ndim - 1]
if (n != p2.tensor.dim[0])
debugger
stop("inner: tensor dimension check")
ndim = p1.tensor.ndim + p2.tensor.ndim - 2
if (ndim > MAXDIM)
stop("inner: rank of result exceeds maximum")
a = p1.tensor.elem
b = p2.tensor.elem
#---------------------------------------------------------------------
#
# ak is the number of rows in tensor A
#
# bk is the number of columns in tensor B
#
# Example:
#
# A[3][3][4] B[4][4][3]
#
# 3 3 ak = 3 * 3 = 9
#
# 4 3 bk = 4 * 3 = 12
#
#---------------------------------------------------------------------
ak = 1
for i in [0...(p1.tensor.ndim - 1)]
ak *= p1.tensor.dim[i]
bk = 1
for i in [1...p2.tensor.ndim]
bk *= p2.tensor.dim[i]
p3 = alloc_tensor(ak * bk)
c = p3.tensor.elem
# new method copied from ginac http://www.ginac.de/
for i in [0...ak]
for j in [0...n]
if (iszero(a[i * n + j]))
continue
for k in [0...bk]
push(a[i * n + j])
push(b[j * bk + k])
multiply()
push(c[i * bk + k])
add()
c[i * bk + k] = pop()
#---------------------------------------------------------------------
#
# Note on understanding "k * bk + j"
#
# k * bk because each element of a column is bk locations apart
#
# + j because the beginnings of all columns are in the first bk
# locations
#
# Example: n = 2, bk = 6
#
# b111 <- 1st element of 1st column
# b112 <- 1st element of 2nd column
# b113 <- 1st element of 3rd column
# b121 <- 1st element of 4th column
# b122 <- 1st element of 5th column
# b123 <- 1st element of 6th column
#
# b211 <- 2nd element of 1st column
# b212 <- 2nd element of 2nd column
# b213 <- 2nd element of 3rd column
# b221 <- 2nd element of 4th column
# b222 <- 2nd element of 5th column
# b223 <- 2nd element of 6th column
#
#---------------------------------------------------------------------
if (ndim == 0)
push(p3.tensor.elem[0])
else
p3.tensor.ndim = ndim
j = 0
for i in [0...(p1.tensor.ndim - 1)]
p3.tensor.dim[i] = p1.tensor.dim[i]
j = p1.tensor.ndim - 1
for i in [0...(p2.tensor.ndim - 1)]
p3.tensor.dim[j + i] = p2.tensor.dim[i + 1]
push(p3)
# Algebrite.run('c·(b+a)ᵀ·inv((a+b)ᵀ)·d').toString();
# Algebrite.run('c*(b+a)ᵀ·inv((a+b)ᵀ)·d').toString();
# Algebrite.run('(c·(b+a)ᵀ)·(inv((a+b)ᵀ)·d)').toString();
get_innerprod_factors = (tree, factors_accumulator) ->
# console.log "extracting inner prod. factors from " + tree
if !iscons(tree)
add_factor_to_accumulator(tree, factors_accumulator)
return
if cdr(tree) == symbol(NIL)
tree = get_innerprod_factors(car(tree), factors_accumulator)
return
if isinnerordot(tree)
# console.log "there is inner at top, recursing on the operands"
get_innerprod_factors(car(cdr(tree)),factors_accumulator)
get_innerprod_factors(cdr(cdr(tree)),factors_accumulator)
return
add_factor_to_accumulator(tree, factors_accumulator)
add_factor_to_accumulator = (tree, factors_accumulator) ->
if tree != symbol(NIL)
# console.log ">> adding to factors_accumulator: " + tree
factors_accumulator.push(tree)