--- title: "Exterior calculus with R" author: "Robin K. S. Hankin" output: html_vignette bibliography: stokes.bib vignette: > %\VignetteIndexEntry{The exterior calculus} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- # The `stokes` package ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE) options(rmarkdown.html_vignette.check_title = FALSE) library("stokes") set.seed(1) ``` ```{r out.width='20%', out.extra='style="float:right; padding:10px"',echo=FALSE} knitr::include_graphics(system.file("help/figures/stokes.png", package = "stokes")) ``` To cite the `stokes` package in publications please use [@hankin2022_stokes]. Ordinary differential calculus may be formalized and generalized to arbitrary-dimensional oriented manifolds using the exterior calculus. Here I show how the `stokes` package furnishes functionality for working with the exterior calculus, and provide numerical verification of a number of theorems. Notation follows that of @spivak1965, and @hubbard2015. Recall that a $k$-tensor is a multilinear map $S\colon V^k\longrightarrow\mathbb{R}$, where $V=\mathbb{R}^n$ is considered as a vector space; Spivak denotes the space of multilinear maps as $\mathcal{J}^k(V)$. Formally, multilinearity means \[ S{\left(v_1,\ldots,av_i,\ldots,v_k\right)} = a\cdot S{\left(v_1,\ldots,v_i,\ldots,v_k\right)} \] and \[ S{\left(v_1,\ldots,v_i+{v_i}',\ldots,v_k\right)}=S{\left(v_1,\ldots,v_i,\ldots,x_v\right)}+ S{\left(v_1,\ldots,{v_i}',\ldots,v_k\right)}. \] where $v_i\in V$. If $S\in\mathcal{J}^k(V)$ and $T\in\mathcal{J}^l(V)$, then we may define $S\otimes T\in\mathcal{J}^{k+l}(V)$ as \[ S\otimes T{\left(v_1,\ldots,v_k,v_{k+1},\ldots,v_{k+l}\right)}= S{\left(v_1,\ldots,v_k\right)}\cdot T{\left(v_1,\ldots,v_l\right)}. \] Spivak observes that $\mathcal{J}^k(V)$ is spanned by the $n^k$ products of the form \[ \phi_{i_1}\otimes\phi_{i_2}\otimes\cdots\otimes\phi_{i_k}\qquad 1\leq i_i,i_2,\ldots,i_k\leq n \] where $v_1,\ldots,v_k$ is a basis for $V$ and $\phi_i{\left(v_j\right)}=\delta_{ij}$; we can therefore write \[ S=\sum_{1\leq i_1,\ldots,i_k\leq n} a_{i_1\ldots i_k} \phi_{i_1}\otimes\cdots\otimes\phi_{i_k}. \] The space spanned by such products has a natural representation in R as an array of dimensions $n\times\cdots\times n=n^k$. If `A` is such an array, then the element `A[i_1,i_2,...,i_k]` is the coefficient of $\phi_{i_1}\otimes\ldots\otimes\phi_{i_k}$. However, it is more efficient and conceptually cleaner to consider a *sparse* array, as implemented by the `spray` package. We will consider the case $n=5,k=4$, so we have multilinear maps from $\left(\mathbb{R}^5\right)^4$ to $\mathbb{R}$. Below, we will test algebraic identities in R using the idiom furnished by the stokes package. For our example we will define $S=1.5\phi_5\otimes\phi_1\otimes\phi_1\otimes\phi_1+2.5\phi_1\otimes\phi_1\otimes\phi_2\otimes\phi_3+3.5\phi_1\otimes\phi_3\otimes\phi_4\otimes\phi_2$ using a matrix with three rows, one per term, and whose rows correspond to each term's tensor products of the $\phi$'s. We first have to load the `stokes` package: ```{r,label=loadstokeslibrary,message=FALSE} library("stokes") ``` Then the idiom is straightforward: ```{r label=straightforwardidiom} k <- 4 n <- 5 M <- matrix(c(5,1,1,1, 1,1,2,3, 1,3,4,2),3,4,byrow=TRUE) M S <- as.ktensor(M,coeffs= 0.5 + 1:3) S ``` Observe that, if stored as an array of size $n^k$, $S$ would have $5^4=625$ elements, all but three of which are zero. So $S$ is a 4-tensor, mapping $V^4$ to $\mathbb{R}$, where $V=\mathbb{R}^5$. Here we have $S=1.5\phi_5\otimes\phi_1\otimes\phi_1\otimes\phi_1+2.5\phi_1\otimes\phi_1\otimes\phi_2\otimes\phi_3+3.5\phi_1\otimes\phi_3\otimes\phi_4\otimes\phi_2$. Note that in some implementations the row order of object `S` will differ from that of `M`; this phenomenon is due to the underlying `C` implementation using the `STL map` class; see the `disordR` package [@hankin2022_disordR] and is discussed in more detail in the `mvp` package [@hankin2022_mvp]. ### Package idiom for evaluation of a tensor First, we will define $E$ to be a random point in $V^k$ in terms of a matrix: ```{r setupsetseed} set.seed(0) (E <- matrix(rnorm(n*k),n,k)) # A random point in V^k ``` Recall that $n=5$, $k=4$, so $E\in\left(\mathbb{R}^5\right)^4$. We can evaluate $S$ at $E$ as follows: ```{r showfandfE} f <- as.function(S) f(E) ``` ### Vector space structure of tensors Tensors have a natural vector space structure; they may be added and subtracted, and multiplied by a scalar, the same as any other vector space. Below, we define a new tensor $S_1$ and work with $2S-3S_1$: ```{r simpletensorarith} S1 <- as.ktensor(1+diag(4),1:4) 2*S-3*S1 ``` We may verify that tensors are linear using package idiom: ```{r verifylinearity} LHS <- as.function(2*S-3*S1)(E) RHS <- 2*as.function(S)(E) -3*as.function(S1)(E) c(lhs=LHS,rhs=RHS,diff=LHS-RHS) ``` (that is, identical up to numerical precision). ### Numerical verification of multilinearity in the package Testing multilinearity is straightforward in the package. To do this, we need to define three matrices `E1,E2,E3` corresponding to points in $\left(\mathbb{R}^5\right)^4$ which are identical except for one column. In `E3`, this column is a linear combination of the corresponding column in `E2` and `E3`: ```{r E1E2E3columns} E1 <- E E2 <- E E3 <- E x1 <- rnorm(n) x2 <- rnorm(n) r1 <- rnorm(1) r2 <- rnorm(1) E1[,2] <- x1 E2[,2] <- x2 E3[,2] <- r1*x1 + r2*x2 ``` Then we can verify the multilinearity of $S$ by coercing to a function which is applied to `E1, E2, E3`: ```{r asfuncS} f <- as.function(S) LHS <- r1*f(E1) + r2*f(E2) RHS <- f(E3) c(lhs=LHS,rhs=RHS,diff=LHS-RHS) ``` (that is, identical up to numerical precision). Note that this is *not* equivalent to linearity over $V^{nk}$: ```{r E1E2matrix} E1 <- matrix(rnorm(n*k),n,k) E2 <- matrix(rnorm(n*k),n,k) LHS <- f(r1*E1+r2*E2) RHS <- r1*f(E1)+r2*f(E2) c(lhs=LHS,rhs=RHS,diff=LHS-RHS) ``` ### Tensor product of general tensors Given two k-tensor objects $S,T$ we can form the tensor product $S\otimes T$, defined as \[ S\otimes T{\left(v_1,\ldots,v_k,v_{k+1},\ldots, v_{k+l}\right)}= S{\left(v_1,\ldots v_k\right)} \cdot T{\left(v_{k+1},\ldots v_{k+l}\right)} \] We will calculate the tensor product of two tensors `S1,S2` defined as follows: ```{r S1S2tensors} (S1 <- ktensor(spray(cbind(1:3,2:4),1:3))) (S2 <- as.ktensor(matrix(1:6,2,3))) ``` The R idiom for $S1\otimes S2$ would be `tensorprod()`, or `%X%`: ```{r S1S2tensorprod} tensorprod(S1,S2) ``` Then, for example: ```{r tensorS1S2verify} E <- matrix(rnorm(30),6,5) LHS <- as.function(tensorprod(S1,S2))(E) RHS <- as.function(S1)(E[,1:2]) * as.function(S2)(E[,3:5]) c(lhs=LHS,rhs=RHS,diff=LHS-RHS) ``` (that is, identical up to numerical precision). # Alternating forms An alternating form is a multilinear map $T$ satisfying \[ T{\left(v_1,\ldots,v_i,\ldots,v_j,\ldots,v_k\right)}= -T{\left(v_1,\ldots,v_j,\ldots,v_i,\ldots,v_k\right)} \] (or, equivalently, $T{\left(v_1,\ldots,v_i,\ldots,v_i,\ldots,v_k\right)}= 0$). We write $\Lambda^k(V)$ for the space of all alternating multilinear maps from $V^k$ to $\mathbb{R}$. Spivak gives $\operatorname{Alt}\colon\mathcal{J}^k(V)\longrightarrow\Lambda^k(V)$ defined by \[\operatorname{Alt}(T)\left(v_1,\ldots,v_k\right)= \frac{1}{k!}\sum_{\sigma\in S_k}\operatorname{sgn}(\sigma)\cdot T{\left(v_{\sigma(1)},\ldots,v_{\sigma(k)}\right)} \] where the sum ranges over all permutations of $\left[n\right]=\left\{1,2,\ldots,n\right\}$ and $\operatorname{sgn}(\sigma)\in\pm 1$ is the sign of the permutation. If $T\in\mathcal{J}^k(V)$ and $\omega\in\Lambda^k(V)$, it is straightforward to prove that $\operatorname{Alt}(T)\in\Lambda^k(V)$, $\operatorname{Alt}\left(\operatorname{Alt}\left(T\right)\right)=\operatorname{Alt}\left(T\right)$, and $\operatorname{Alt}\left(\omega\right)=\omega$. In the stokes package, this is effected by the `Alt()` function: ```{r showAltS1} S1 Alt(S1) ``` Verifying that `S1` is in fact alternating is straightforward: ```{r verifyAltisAlt} E <- matrix(rnorm(8),4,2) Erev <- E[,2:1] as.function(Alt(S1))(E) + as.function(Alt(S1))(Erev) # should be zero ``` However, we can see that this form for alternating tensors (here called $k$-forms) is inefficient and highly redundant: in this example there is a `1 2` term and a `2 1` term (the coefficients are equal and opposite). In this example we have $k=2$ but in general there would be potentially $k!$ essentially repeated terms which collectively require only a single coefficient. The package provides `kform` objects which are inherently alternating using a more efficient representation; they are described using wedge products which are discussed next. ## Wedge products and the exterior calculus This section follows the exposition of Hubbard and Hubbard, who introduce the exterior calculus starting with a discussion of elementary forms, which are alternating forms with a particularly simple structure. An example of an elementary form would be $\mathrm{d}x_1\wedge\mathrm{d}x_3$ [treated as an indivisible entity], which is an alternating multilinear map from $\mathbb{R}^n\times\mathbb{R}^n$ to $\mathbb{R}$ with \[ \left( \mathrm{d}x_1\wedge\mathrm{d}x_3 \right)\left( \begin{pmatrix}a_1\\a_2\\a_3\\ \vdots\\ a_n\end{pmatrix}, \begin{pmatrix}b_1\\b_3\\b_3\\ \vdots\\ b_n\end{pmatrix} \right)=\mathrm{det} \begin{pmatrix} a_1 & b_1 \\ a_3 & b_3\end{pmatrix} =a_1b_3-a_3b_1 \] That this is alternating follows from the properties of the determinant. In general of course, $\mathrm{d}x_i\wedge\mathrm{d}x_j\left( \begin{pmatrix}a_1\\ \vdots\\ a_n\end{pmatrix}, \begin{pmatrix}b_1\\ \vdots\\ b_n\end{pmatrix} \right)=\mathrm{det} \begin{pmatrix} a_i & b_i \\ a_j & b_j\end{pmatrix}$. Because such objects are linear, it is possible to consider sums of elementary forms, such as $d\mathrm{x}_1\wedge\mathrm{d}x_2 + 3 \mathrm{d}x_2\wedge\mathrm{d}x_3$ with \[ \left( \mathrm{d}x_1\wedge\mathrm{d}x_2 + 3\mathrm{d}x_2\wedge\mathrm{d}x_3 \right)\left( \begin{pmatrix}a_1\\a_2\\ \vdots\\ a_n\end{pmatrix}, \begin{pmatrix}b_1\\b_2\\ \vdots\\ b_n\end{pmatrix} \right)=\mathrm{det} \begin{pmatrix} a_1 & b_1\\ a_2 & b_2\end{pmatrix} +3\mathrm{det} \begin{pmatrix} a_2 & b_2\\ a_3 & b_3\end{pmatrix} \] or even $K=\mathrm{d}x_1\wedge\mathrm{d}x_2\wedge\mathrm{d}x_3 +5\mathrm{d}x_1\wedge\mathrm{d}x_2\wedge\mathrm{d}x_4$ which would be a linear map from $\left(\mathbb{R}^n\right)^3$ to $\mathbb{R}$ with \[ \left( \mathrm{d}x_4\wedge\mathrm{d}x_2\wedge\mathrm{d}x_3 +5\mathrm{d}x_1\wedge\mathrm{d}x_2\wedge\mathrm{d}x_4 \right)\left( \begin{pmatrix}a_1\\a_2\\ \vdots\\ a_n\end{pmatrix}, \begin{pmatrix}b_1\\b_2\\ \vdots\\ b_n\end{pmatrix}, \begin{pmatrix}c_1\\c_2\\ \vdots\\ c_n\end{pmatrix} \right)=\mathrm{det} \begin{pmatrix} a_4 & b_4 & c_4\\ a_2 & b_2 & c_2\\ a_3 & b_3 & c_3 \end{pmatrix} +5\mathrm{det} \begin{pmatrix} a_1 & b_1 & c_1\\ a_2 & b_2 & c_2\\ a_4 & b_4 & c_4 \end{pmatrix}. \] Defining $K$ has ready R idiom in which we define a matrix whose rows correspond to the differentials in each term: ```{r usedifferentials} M <- matrix(c(4,2,3,1,4,2),2,3,byrow=TRUE) M K <- as.kform(M,c(1,5)) K ``` Function `as.kform()` takes each row of `M` and places the elements in increasing order; the coefficient will change sign if the permutation is odd. Note that the order of the rows in `K` is immaterial and indeed in some implementations will appear in a different order: the stokes package uses the `spray` package, which in turn utilises the STL map class of C++. ## Formal definition of dx In the previous section we defined objects such as "$\mathrm{d}x_1\wedge\mathrm{d}x_6$" as a single entity. Here I define the elementary form $\mathrm{d}x_i$ formally and in the next section discuss the wedge product $\wedge$. The elementary form $\mathrm{d}x_i$ is simply a map from $\mathbb{R}^n$ to $\mathbb{R}$ with $\mathrm{d}x_i{\left(x_1,x_2,\ldots,x_n\right)}=x_i$. Observe that $\mathrm{d}x_i$ is an alternating form, even though we cannot swap arguments (because there is only one). Package idiom for creating an elementary form appears somewhat cryptic at first sight, but is consistent (it is easier to understand package idiom for creating more complicated alternating forms, as in the next section). Suppose we wish to work with $\mathrm{d}x_3$: ```{r dx3inmatrixform} dx3 <- as.kform(matrix(3,1,1),1) options(kform_symbolic_print = NULL) # revert to default print method dx3 ``` Interpretation of the output above is not obvious (it is easier to understand the output from more complicated alternating forms, as in the next section), but for the moment observe that $\mathrm{d}x_3$ is indeed an alternating form, mapping $\mathbb{R}^n$ to $\mathbb{R}$ with $\mathrm{d}x_3{\left(x_1,x_2,\ldots,x_n\right)}=x_3$. Thus, for example: ```{r showdx3beingused} as.function(dx3)(c(14,15,16)) as.function(dx3)(c(14,15,16,17,18)) # idiom can deal with arbitrary vectors ``` and we see that $\mathrm{d}x_3$ picks out the third element of a vector. These are linear in the sense that we may add and subtract these elementary forms: ```{r dx5askform} dx5 <- as.kform(matrix(5,1,1),1) as.function(dx3 + 2*dx5)(1:10) # picks out element 3 + 2*element 5 ``` ## Formal definition of wedge product The wedge product maps two alternating forms to another alternating form; given $\omega\in\Lambda^k(V)$ and $\eta\in\Lambda^l(V)$, Spivak defines the wedge product $\omega\wedge\eta\in\Lambda^{k+l}(V)$ as \[ \omega\wedge\eta={k+l\choose k\quad l}\operatorname{Alt}(\omega\otimes\eta) \] and this is implemented in the package by function `wedge()`, or, more idiomatically, `^`: ```{r M1K1M2K2} M1 <- matrix(c(3,5,4, 4,6,1),2,3,byrow=TRUE) K1 <- as.kform(M1,c(2,7)) K1 M2 <- cbind(1:5,3:7) K2 <- as.kform(M2,1:5) K2 ``` In symbolic notation, `K1` is equal to $7\mathrm{d}x_1\wedge\mathrm{d}x_4\wedge\mathrm{d}x_6 -2\mathrm{d}x_3\wedge\mathrm{d}x_4\wedge\mathrm{d}x_5$. and `K2` is $\mathrm{d}x_1\wedge\mathrm{d}x_3+ 2\mathrm{d}x_2\wedge\mathrm{d}x_4+ 3\mathrm{d}x_3\wedge\mathrm{d}x_5+ 4\mathrm{d}x_4\wedge\mathrm{d}x_6+ 5\mathrm{d}x_5\wedge \mathrm{d}x_7$. Package idiom for wedge products is straightforward: ```{r K1wedgeK2} K1 ^ K2 ``` (we might write the product as $-35\mathrm{d}x_1\wedge\mathrm{d}x_4\wedge\mathrm{d}x_5\wedge \mathrm{d}x_6\wedge\mathrm{d}x_7 -21\mathrm{d}x_1\wedge\mathrm{d}x_3\wedge\mathrm{d}x_4\wedge\mathrm{d}x_5\wedge\mathrm{d}x_6$). See how the wedge product eliminates rows with repeated entries, gathers permuted rows together (respecting the sign of the permutation), and expresses the result in terms of elementary forms. The product is a linear combination of two elementary forms; note that only two coefficients out of a possible ${7\choose 5}=21$ are nonzero. Note again that the order of the rows in the product is arbitrary. The wedge product has formal properties such as distributivity but by far the most interesting one is associativity, which I will demonstrate below: ```{r F1F2F3kforms} F1 <- as.kform(matrix(c(3,4,5, 4,6,1,3,2,1),3,3,byrow=TRUE)) F2 <- as.kform(cbind(1:6,3:8),1:6) F3 <- kform_general(1:8,2) (F1 ^ F2) ^ F3 F1 ^ (F2 ^ F3) ``` Note carefully in the above that the terms in `(F1 ^ F2) ^ F3` and `F1 ^ (F2 ^ F3)` appear in a different order. They are nevertheless algebraically identical, as we may demonstrate by calculating their difference: ```{r wedgearithmeticF1F2F3} (F1 ^ F2) ^ F3 - F1 ^ (F2 ^ F3) ``` Spivak observes that $\Lambda^k(V)$ is spanned by the $n\choose k$ wedge products of the form \[ \mathrm{d}x_{i_1}\wedge\mathrm{d}x_{i_2}\wedge\ldots\wedge\mathrm{d}x_{i_k}\qquad 1\leq i_i1$; we specify $\phi_\mathbf{v}=\phi(\mathbf{v})$ if $k=1$. Verification is straightforward: ```{r lookatfunc} (o <- rform()) # a random 3-form V <- matrix(runif(21),ncol=3) LHS <- as.function(o)(V) RHS <- as.function(contract(o,V[,1]))(V[,-1]) c(LHS=LHS,RHS=RHS,diff=LHS-RHS) ``` It is possible to iterate the contraction process; if we pass a matrix $V$ to `contract()` then this is interpreted as repeated contraction with the columns of $V$: ```{r coerceotoafunction} as.function(contract(o,V[,1:2]))(V[,-(1:2),drop=FALSE]) ``` If we pass three columns to `contract()` the result is a $0$-form: ```{r contractovlosetrue} contract(o,V) ``` In the above, the result is coerced to a scalar; in order to work with a formal $0$-form (which is represented in the package as a `spray` with a zero-column index matrix) we can use the `lose=FALSE` argument: ```{r contractovlosefalse} contract(o,V,lose=FALSE) ``` ## Transformations and pullback Suppose we are given a two-form $\omega=\sum_{i pullback(M) |> pullback(solve(M)) ``` Above we see many rows with values small enough for the print method to print an exact zero, but not sufficiently small to be eliminated by the `spray` internals. We can remove the small entries with `zap()`: ```{r zapsmallroundofferrors} o |> pullback(M) |> pullback(solve(M)) |> zap() ``` See how the result is equal to the original $k$-form $2dy_2\wedge dy_4\wedge dy_5$. ## Exterior derivatives Given a $k$-form $\omega$, Spivak defines the differential of $\omega$ to be a $(k+1)$-form $\mathrm{d}\omega$ as follows. If \[ \omega = \sum_{ i_1 < i_2 <\cdots