vector_cross_product()
and vcp3() in the stokes packagevignettes/vector_cross_product.Rmd
vector_cross_product.Rmd
vector_cross_productfunction (M)
{
stopifnot(is.matrix(M))
n <- nrow(M)
stopifnot(n == ncol(M) + 1)
(-1)^n * sapply(seq_len(n), function(i) {
(-1)^i * det(M[-i, , drop = FALSE])
})
}
vcp3function(u,v){hodge(as.1form(u)^as.1form(v))}
To cite the stokes package in publications, please use
Hankin (2022b); this function monograph
discusses vector_cross_product(). The vector cross
product of two vectors
,
denoted
,
is defined in elementary mechanics as
,
where
is the angle between
and
,
and
is the unit vector perpendicular to
and
such that
is positively oriented. Vector cross products find wide applications in
physics, engineering, and computer science. Spivak (1965)
considers the standard vector cross product
and places it in a more general and rigorous context. In a memorable
passage, he states:
If and is defined by
then ; therefore there is a unique such that
This is denoted and is called the cross product of .
- Michael Spivak, 1969 (Calculus on Manifolds, Perseus books). Pages 83-84
The reason for being at the bottom rather than the top is to ensure that the -tuple has positive orientation with respect to the standard basis vectors of . In we get the standard elementary mnemonic for , :
This is (universal) shorthand for the formal definition of the cross product, although sometimes it is better to return to Spivakโs formulation and, writing , use the definition directly obtaining
In the stokes package (Hankin 2022b), R function
vector_cross_product() takes a matrix with
rows and
columns: the transpose of the work above. This is because
stokes (and R) convention is to interpret
columns of a matrix as vectors. If we wanted to take the cross
product of
with
:
## [,1] [,2]
## [1,] 5 1
## [2,] -2 2
## [3,] 1 0
## [1] -2 1 12
But of course we can work with higher dimensional spaces:
vector_cross_product(matrix(rnorm(30),6,5))## [1] 4.715354 -1.003152 -12.051733 3.459023 -12.902338 -6.943296
In the case the vector cross product becomes a unary operator of a single vector , returning its argument rotated counterclockwise by ; this case is discussed at the end, along with .
We can demonstrate that the function has the correct orientation. We need to ensure that the vectors constitute a right-handed basis:
det(cbind(M,vector_cross_product(M)))>0## [1] TRUE
So it is right-handed in this case. Here is a more severe test of the right-handedness::
f <- function(n){
M <- matrix(rnorm(n^2+n),n+1,n)
det(cbind(M,vector_cross_product(M)))>0
}
all(sapply(sample(3:10,100,replace=TRUE),f))## [1] TRUE
Above, we see that in each case the vectors are right-handed. We may further verify that the rules for determinants are being obeyed by taking a dot product as follows:
M <- matrix(rnorm(42),7,6)
crossprod(M,vector_cross_product(M))## [,1]
## [1,] 1.578598e-15
## [2,] 1.110223e-16
## [3,] 4.440892e-16
## [4,] -3.330669e-15
## [5,] 1.332268e-15
## [6,] -5.440093e-15
Writing
,
,
we see that the dot product
as implemented by crossprod() vanishes (up to numerical
precision), as the determinants in question have two identical
columns.
Spivak gives the following properties:
For the first we use a permutation sigma from the
permutations package (Hankin 2020) with a sign of
:
## [1] -1
Mdash <- M[,as.function(sigma)(seq_len(5))]
vector_cross_product(M) + vector_cross_product(Mdash)## [1] 0.000000e+00 5.551115e-17 1.332268e-15 -1.776357e-15 0.000000e+00
## [6] 0.000000e+00
Above we see that the the two vector cross products add to zero (up
to numerical precision), as they should because sigma is an
odd permutation. For the second:
Mdash <- M
Mdash[,3] <- pi*Mdash[,3]
vector_cross_product(Mdash) - vector_cross_product(M) * pi## [1] 1.065814e-14 -8.881784e-16 -3.552714e-15 7.105427e-15 -1.776357e-15
## [6] 0.000000e+00
Above we see that the second product is times the first (to numerical precision), by linearity of the vector cross product. For the third:
M1 <- M
M2 <- M
Msum <- M
v1 <- runif(6)
v2 <- runif(6)
M1[,3] <- v1
M2[,3] <- v2
Msum[,3] <- v1+v2
vector_cross_product(M1) + vector_cross_product(M2) - vector_cross_product(Msum)## [1] 0.000000e+00 2.220446e-16 0.000000e+00 -3.552714e-15 1.776357e-15
## [6] 1.942890e-16
Above we see that the sum of the first two products is equal to that of the third (up to numerical precision), again by linearity of the vector cross product.
The cross product has a coordinate-free definition as the Hodge conjugate of the wedge product of its arguments. In dimensions:
This is not used in function vector_cross_product()
because it is computationally inefficient and (I think) prone to
numerical roundoff errors. We may verify that the definitions agree,
using a six-dimensional test case:
set.seed(2)
M <- matrix(rnorm(30),6,5)
(ans1 <- vector_cross_product(M))## [1] 4.431826 -1.966102 -3.344998 -6.853352 -11.879641 7.170485
We can see that vector_cross_product() returns an R
vector. To verify that this is correct, we compare the output with the
value calculated directly with the wedge product:
## An alternating linear map from V^5 to R with V=R^6:
## val
## 1 2 3 4 5 = 7.170485
## 1 2 4 5 6 = 3.344998
## 1 2 3 5 6 = -6.853352
## 1 3 4 5 6 = -1.966102
## 2 3 4 5 6 = -4.431826
## 1 2 3 4 6 = 11.879641
(ans2 <- hodge(jj))## An alternating linear map from V^1 to R with V=R^6:
## val
## 5 = -11.879641
## 1 = 4.431826
## 2 = -1.966102
## 4 = -6.853352
## 3 = -3.344998
## 6 = 7.170485
Above we see agreement between ans1 and
ans2 although the elements might appear in a different
order (as per disordR discipline). Actually it is possible
to produce the same answer using slightly slicker idiom:
## An alternating linear map from V^1 to R with V=R^6:
## val
## 4 = -6.853352
## 3 = -3.344998
## 1 = 4.431826
## 5 = -11.879641
## 2 = -1.966102
## 6 = 7.170485
[again note the different order in the output]. Above, we see that
the output of vector_cross_product() [ans1] is
an ordinary R vector, but the direct result [ans2] is a
1-form. In order to compare these, we first need to coerce
ans2 to a 1-form and then subtract:
(diff <- as.1form(ans1) - ans3)## An alternating linear map from V^1 to R with V=R^6:
## val
## 1 = 3.552714e-15
## 2 = 1.776357e-15
## 3 = -1.332268e-15
## 4 = 1.776357e-15
## 5 = 7.105427e-15
## 6 = -4.440892e-15
coeffs(diff)## A disord object with hash 8e9298ff6d77253137fe25d06acabd9c369e4321 and elements
## [1] 3.552714e-15 1.776357e-15 -1.332268e-15 1.776357e-15 7.105427e-15
## [6] -4.440892e-15
## (in some order)
Above we see that ans1 and ans3 match to
within numerical precision.
Taking Spivakโs definition at face value, we could define the vector cross product of three-vectors and as a map from the tangent space to the reals, with , where is the 3-volume element and subscripts refer to contraction. Package idiom for this would be:
However, note that 3D vector cross products are implemented in the
package as function vcp3(), which uses different idiom:
vcp3## function(u,v){hodge(as.1form(u)^as.1form(v))}
This is preferable on the grounds that coercion to a 1-form is explicit. Suppose we wish to take the vector cross product of and :
## An alternating linear map from V^1 to R with V=R^3:
## val
## 1 = 18
## 2 = -1
## 3 = -7
Above, note the order of the lines is implementation-specific as per
disordR discipline (Hankin 2022a), but the form
itself should agree with basis vector evaluation given below. Object
p is the vector cross product of
and
,
but is given as a one-form. We can see the mnemonic in operation by
coercing p to a function and then evaluating this on the
three basis vectors of
:
ucv <- as.function(p)
c(i=ucv(ex), j=ucv(ey), k=ucv(ez))## i j k
## 18 -1 -7
and we see agreement with the mnemonic
.
Further, we may evaluate the triple cross product
by evaluating ucv() at
.
w <- c(1,-3,2)
ucv(w)## [1] 7
This shows agreement with the elementary mnemonic , as expected from linearity.
The following identities are standard results:
We may verify all four together:
x <- c(-6,5,7) # u,v,w as before
c(
hodge(as.1form(u) ^ vcp3(v,w)) == as.1form(v*sum(w*u) - w*sum(u*v)),
hodge(vcp3(u,v) ^ as.1form(w)) == as.1form(v*sum(w*u) - u*sum(v*w)),
as.1form(as.function(vcp3(v,w))(u)*u) == hodge(vcp3(u,v) ^ vcp3(u,w)) ,
hodge(hodge(vcp3(u,v)) ^ vcp3(w,x)) == sum(u*w)*sum(v*x) - sum(u*x)*sum(v*w)
) ## [1] TRUE TRUE TRUE TRUE
Function vector_cross_product() takes a matrix with
rows and
columns. Here I consider the cases
and
.
Firstly,
.
Going back to Spivakโs definition, we see that the cross product is a
unary operation which takes a single vector
;
we might write
.
Formally we define
and seek a vector such that . Thus and we see . Numerically:
vector_cross_product(rbind(4,7))## [1] -7 4
We see that the vector cross product of a single vector is vector rotated by counterclockwise; the dot product of with is zero.
Now we try the even more peculiar case , corresponding to a matrix with one row and zero columns. Formally, the cross product is a nullary operation which takes zero vectors and returns a โvectorโ . The vector cross product in the case is thus a scalar. Again following Spivak we see that is a map from to the reals, with ; we then seek such that ; so and then . Matrices with zero columns and one row are easily created in R:
M <- matrix(data=NA,nrow=1,ncol=0)
M##
## [1,]
dput(M)## structure(logical(0), dim = 1:0)
Function vector_cross_product() takes such an
argument:
## [1] 1
thus returning scalar 1 as intended. Examining the body of
vector_cross_product at the head of the document we see
that the function boils down to returning the determinant of
M[-1,,drop=FALSE]## <0 x 0 matrix>
The determinant of a zero-by-zero matrix is equal to 1 [any zero by zero matrix maps to itself and is thus the identity map, which has by definition a determinant of 1]. Numerically:
## [1] 1
disordR Package.โ https://arxiv.org/abs/2210.03856; arXiv. https://doi.org/10.48550/ARXIV.2210.03856.