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 Spivak, and Hubbard and Hubbard.

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\). 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 terms’s cross products of the \(\phi\)’s. We first have to load the `stokes`

package:

`library("stokes")`

Then the idiom is straightforward:

```
k <- 4
n <- 5
M <- matrix(c(5,1,1,1, 1,1,2,3, 1,3,4,2),3,4,byrow=TRUE)
M
```

```
## [,1] [,2] [,3] [,4]
## [1,] 5 1 1 1
## [2,] 1 1 2 3
## [3,] 1 3 4 2
```

```
S <- as.ktensor(M,coeffs= 0.5 + 1:3)
S
```

```
## val
## 1 3 4 2 = 3.5
## 5 1 1 1 = 1.5
## 1 1 2 3 = 2.5
```

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 and is discussed in more detail in the `mvp`

package.

First, we will define \(E\) to be a random point in \(V^k\) in terms of a matrix:

```
set.seed(0)
(E <- matrix(rnorm(n*k),n,k)) # A random point in V^k
```

```
## [,1] [,2] [,3] [,4]
## [1,] 1.2629543 -1.539950042 0.7635935 -0.4115108
## [2,] -0.3262334 -0.928567035 -0.7990092 0.2522234
## [3,] 1.3297993 -0.294720447 -1.1476570 -0.8919211
## [4,] 1.2724293 -0.005767173 -0.2894616 0.4356833
## [5,] 0.4146414 2.404653389 -0.2992151 -1.2375384
```

Recall that \(n=5\), \(k=4\), so \(E\in\left(\mathbb{R}^5\right)^4\). We can evaluate \(S\) at \(E\) as follows:

```
f <- as.function(S)
f(E)
```

`## [1] -3.068997`

Tensors have a natural vector space structure; they may be added and subtracted, and multiplied by a scalar, as for any other vectors. Thus:

```
S1 <- as.ktensor(1+diag(4),1:4)
2*S-3*S1
```

```
## val
## 1 2 1 1 = -6
## 5 1 1 1 = 3
## 1 3 4 2 = 7
## 1 1 2 1 = -9
## 1 1 2 3 = 5
## 2 1 1 1 = -3
## 1 1 1 2 = -12
```

```
way1 <- as.function(2*S-3*S1)(E)
way2 <- 2*as.function(S)(E) -3*as.function(S1)(E)
c(way1,way2)
```

`## [1] 2.374816 2.374816`

(that is, identical up to numerical precision).

Testing multilinearity is straightforward in the package. To do this, we need to define three 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`

:

```
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`

:

```
f <- as.function(S)
way1 <- r1*f(E1) + r2*f(E2)
way2 <- f(E3)
c(way1,way2)
```

`## [1] -0.5640577 -0.5640577`

(that is, identical up to numerical precision). Note that this is *not* equivalent to linearity over \(V^{nk}\):

```
E1 <- matrix(rnorm(n*k),n,k)
E2 <- matrix(rnorm(n*k),n,k)
way1 <- f(r1*E1+r2*E2)
way2 <- r1*f(E1)+r2*f(E2)
c(way1,way2)
```

`## [1] 0.1731245 0.3074186`

Given two k-tensor objects \(S,T\) we can form the cross 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 cross product of two tensors `S1,S2`

defined as follows:

`(S1 <- ktensor(spray(cbind(1:3,2:4),1:3)))`

```
## val
## 3 4 = 3
## 1 2 = 1
## 2 3 = 2
```

`(S2 <- as.ktensor(matrix(1:6,2,3)))`

```
## val
## 2 4 6 = 1
## 1 3 5 = 1
```

The R idiom for \(S1\otimes S2\) would be `cross()`

, or `%X%`

:

`cross(S1,S2)`

```
## val
## 2 3 1 3 5 = 2
## 3 4 1 3 5 = 3
## 2 3 2 4 6 = 2
## 1 2 1 3 5 = 1
## 3 4 2 4 6 = 3
## 1 2 2 4 6 = 1
```

Then, for example:

```
E <- matrix(rnorm(30),6,5)
way1 <- as.function(cross(S1,S2))(E)
way2 <- as.function(S1)(E) * as.function(S2)(E)
c(way2,way2)
```

`## [1] 2.00319 2.00319`

(that is, identical up to numerical precision).

An alternating form is a multilinear map \(T\) satisfying

\[ \mathrm{T}\left(v_1,\ldots,v_i,\ldots,v_j,\ldots,v_k\right)= -\mathrm{T}\left(v_1,\ldots,v_j,\ldots,v_i,\ldots,v_k\right) \]

(or, equivalently, \(\mathrm{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:

`S1`

```
## val
## 3 4 = 3
## 1 2 = 1
## 2 3 = 2
```

`Alt(S1)`

```
## val
## 1 2 = 0.5
## 2 3 = 1.0
## 2 1 = -0.5
## 3 2 = -1.0
## 3 4 = 1.5
## 4 3 = -1.5
```

Verifying that `S1`

is in fact alternating is straightforward:

```
E <- matrix(rnorm(8),4,2)
Erev <- E[,2:1]
as.function(Alt(S1))(E) + as.function(Alt(S1))(Erev) # should be zero
```

`## [1] 0`

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.

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 elementary form is an alternating multilinear map with a particularly simple structure. An example of an elementary form would be \(dx_1\wedge dx_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( dx_1\wedge dx_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, \(dx_i\wedge dx_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 \(dx_1\wedge dx_2 + 3 dx_2\wedge dx_3\) with

\[ \left( dx_1\wedge dx_2 + 3dx_2\wedge dx_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=dx_1\wedge dx_2\wedge dx_3 +5dx_1\wedge dx_2\wedge dx_4\) which would be a linear map from \(\left(\mathbb{R}^n\right)^3\) to \(\mathbb{R}\) with

\[ \left( dx_4\wedge dx_2\wedge dx_3 +5dx_1\wedge dx_2\wedge dx_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:

```
M <- matrix(c(4,2,3,1,2,4),2,3,byrow=TRUE)
M
```

```
## [,1] [,2] [,3]
## [1,] 4 2 3
## [2,] 1 2 4
```

```
K <- as.kform(M,c(1,5))
K
```

```
## val
## 1 2 4 = 5
## 2 3 4 = 1
```

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++.

In the previous section we defined objects such as “\(dx_1\wedge dx_6\)” as a single entity. Here I define the elementary form \(dx_i\) and the wedge product \(\wedge\) formally. The elementary form \(dx_i\) is simply a map from \(\mathbb{R}^n\) to \(\mathbb{R}\) with \(dx_i\left(x_1,x_2,\ldots,x_n\right)=x_i\). 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, `%^%`

:

```
M1 <- matrix(c(3,4,5, 4,6,1),2,3,byrow=TRUE)
K1 <- as.kform(M1,c(2,7))
K1
```

```
## val
## 1 4 6 = 7
## 3 4 5 = 2
```

```
M2 <- cbind(1:5,3:7)
K2 <- as.kform(M2,1:5)
K2
```

```
## val
## 4 6 = 4
## 5 7 = 5
## 3 5 = 3
## 2 4 = 2
## 1 3 = 1
```

`K1 %^% K2`

```
## val
## 1 4 5 6 7 = -35
## 1 3 4 5 6 = -21
```

(we might write the product as \(-35dx_1\wedge dx_4\wedge dx_5\wedge dx_6\wedge dx_7 -21dx_1\wedge dx_3\wedge dx_4\wedge dx_5\wedge dx_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:

```
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
```

```
## val
## 1 2 3 4 5 7 8 = -5
## 1 2 3 4 5 6 7 = 1
## 1 3 4 5 6 7 8 = -2
## 1 2 4 5 6 7 8 = -5
## 1 2 3 4 5 6 8 = 1
## 1 2 3 5 6 7 8 = 11
## 1 2 3 4 6 7 8 = 2
## 2 3 4 5 6 7 8 = 6
```

`F1 %^% (F2 %^% F3)`

```
## val
## 1 2 3 4 5 7 8 = -5
## 1 2 4 5 6 7 8 = -5
## 1 2 3 4 5 6 8 = 1
## 1 3 4 5 6 7 8 = -2
## 1 2 3 5 6 7 8 = 11
## 1 2 3 4 6 7 8 = 2
## 2 3 4 5 6 7 8 = 6
## 1 2 3 4 5 6 7 = 1
```

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:

`(F1 %^% F2) %^% F3 - F1 %^% (F2 %^% F3)`

`## empty sparse array with 7 columns`

Spivak observes that \(\Lambda^k(V)\) is spanned by the \(n\choose k\) wedge products of the form

\[ dx_{i_1}\wedge dx_{i_2}\wedge\ldots\wedge dx_{i_k}\qquad 1\leq i_i<i_2<\cdots <i_k\leq n \]

where these products are the elementary forms (compare \(\mathcal{J}^k(V)\), which is spanned by \(n^k\) elementary forms). Formally, multilinearity means every element of the space \(\Lambda^k(V)\) is a linear combination of elementary forms, as illustrated in the package by function `kform_general()`

. Consider the following idiom:

```
Krel <- kform_general(4,2,1:6)
Krel
```

```
## val
## 2 4 = 5
## 1 4 = 4
## 3 4 = 6
## 2 3 = 3
## 1 3 = 2
## 1 2 = 1
```

Object `Krel`

is a two-form, specifically a map from \(\left(\mathbb{R}^4\right)^2\) to \(\mathbb{R}\). Observe that `Krel`

has \({4\choose 2}=6\) components, which do not appear in any particular order. Addition of such \(k\)-forms is straightforward in R idiom but algebraically nontrivial:

```
K1 <- as.kform(matrix(1:4,2,2),1:2)
K2 <- as.kform(matrix(c(1,3,7,8,2,4),ncol=2,byrow=TRUE),c(-1,5,4))
K1
```

```
## val
## 2 4 = 2
## 1 3 = 1
```

`K2`

```
## val
## 1 3 = -1
## 2 4 = 4
## 7 8 = 5
```

`K1+K2`

```
## val
## 2 4 = 6
## 7 8 = 5
```

In the above, note how the \(dx_2\wedge dx_4\) terms combine [to give `2 4 = 6`

] and the \(dx_1\wedge dx_3\) term vanishes.

Although the spray form used above is probably the most direct and natural representation of differential forms in numerical work, sometimes we need a more algebraic print method.

```
U <- ktensor(spray(cbind(1:4,2:5),1:4))
U
```

```
## val
## 4 5 = 4
## 3 4 = 3
## 1 2 = 1
## 2 3 = 2
```

we can represent this more algebraically using the `as.symbolic()`

function:

`as.symbolic(U)`

`## [1] +4 d*e +3 c*d + a*b +2 b*c`

In the above, `U`

is a multilinear map from \(\left(\mathbb{R}^5\right)^2\) to \(\mathbb{R}\). Symbolically, `a`

represents the map that takes \((a,b,c,d,e)\) to \(a\), `b`

the map that takes \((a,b,c,d,e)\) to `b`

, and so on. The asterisk `*`

represents the tensor product \(\otimes\). Alternating forms work similarly but \(k\)-forms have different defaults:

```
K <- kform_general(3,2,1:3)
K
```

```
## val
## 1 2 = 1
## 1 3 = 2
## 2 3 = 3
```

`as.symbolic(K,d="d",symbols=letters[23:26])`

`## [1] + dw^dx +2 dw^dy +3 dx^dy`

Note that the wedge product \(\wedge\), although implemented in package idiom as `%^%`

, appears in the symbolic representation as an ascii caret, `^`

.

Given a \(k\)-form \(\phi\colon V^k\longrightarrow\mathbb{R}\) and a vector \(\mathbf{v}\in V\), the *contraction* \(\phi_\mathbf{v}\) of \(\phi\) and \(\mathbf{v}\) is a \(k-1\)-form with

\[ \phi_\mathbf{v}\left(\mathbf{v}^1,\ldots,\mathbf{v}^{k-1}\right) = \phi\left(\mathbf{v},\mathbf{v}^1,\ldots,\mathbf{v}^{k-1}\right) \]

if \(k>1\); we specify \(\phi_\mathbf{v}=\phi(\mathbf{v})\) if \(k=1\). Verification is straightforward:

`(o <- rform()) # a random 3-form`

```
## val
## 1 5 7 = -2
## 1 2 4 = 1
## 4 6 7 = 1
## 5 6 7 = 1
## 1 4 6 = 1
## 2 4 6 = -1
## 1 3 7 = 1
## 2 3 7 = -1
```

```
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)
```

```
## LHS RHS diff
## -9.733146e-02 -9.733146e-02 5.551115e-17
```

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\):

`as.function(contract(o,V[,1:2]))(V[,-(1:2),drop=FALSE])`

`## [1] -0.09733146`

If we pass three columns to `contract()`

the result is a \(0\)-form:

`contract(o,V)`

`## [1] -0.09733146`

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:

`contract(o,V,lose=FALSE)`

```
## val
## = -0.0973315
```

Suppose we are given a two-form \(\omega=\sum_{i<j}a_{ij}dx_i\wedge dx_j\) and relationships \(dx_i=\sum_rM_{ir}dy_r\), then we would have

\[ \omega = \sum_{i<j} a_{ij}\left(\sum_rM_{ir}dy_r\right)\wedge\left(\sum_rM_{jr}dy_r\right). \]

The general situation would be a \(k\)-form where we would have

\[ \omega=\sum_{i_1<\cdots<i_k}a_{i_1\ldots i_k}dx_{i_1}\wedge\cdots\wedge dx_{i_k} \]

giving

\[\omega = \sum_{i_1<\cdots <i_k}\left[ a_{i_1<\cdots < i_k}\left(\sum_rM_{i_1r}dy_r\right)\wedge\cdots\wedge\left(\sum_rM_{i_kr}dy_r\right)\right]. \]

So \(\omega\) was given in terms of \(dx_1,\ldots,dx_k\) and we have expressed it in terms of \(dy_1,\ldots,dy_k\). So for example if

\[ \omega= dx_1\wedge dx_2 + 5dx_1\wedge dx_3\]

and

\[ \left( \begin{array}{l} dx_1\\ dx_2\\ dx_3 \end{array} \right)= \left( \begin{array}{ccc} 1 & 4 & 7\\ 2 & 5 & 8\\ 3 & 6 & 9\\ \end{array} \right) \left( \begin{array}{l} dy_1\\ dy_2\\ dy_3 \end{array} \right) \]

then

\[ \begin{array}{ccl} \omega &=& \left(1dy_1+4dy_2+7dy_3\right)\wedge \left(2dy_1+5dy_2+8dy_3\right)+ 5\left(1dy_1+4dy_2+7dy_3\right)\wedge \left(3dy_1+6dy_2+9dy_3\right) \\ &=&2dy_1\wedge dy_1+5dy_1\wedge dy_2+\cdots+ 5\cdot 7\cdot 6dx_3\wedge dx_2+ 5\cdot 7\cdot 9dx_3\wedge dx_3+\\ &=& -33dy_1\wedge dy_2-66dy_1\wedge dy_3-33dy_2\wedge dy_3 \end{array} \]

Function `transform()`

function does all this but it is slow. I am not 100% sure that there isn’t a much more efficient way to do such a transformation. There are a few tests in `tests/testthat`

.

`(o <- 2 * as.kform(2) %^% as.kform(4) %^% as.kform(5))`

```
## val
## 2 4 5 = 2
```

`M <- matrix(rnorm(25),5,5)`

Then we will transform according to matrix `M`

and then transform according to the matrix inverse; the functionality works nicely with magrittr pipes:

```
library("magrittr")
(o %<>% transform(M) %>% transform(solve(M)))
```

```
## val
## 1 3 5 = 0
## 2 3 4 = 0
## 1 2 3 = 0
## 2 4 5 = 2
## 1 3 4 = 0
## 3 4 5 = 0
## 1 4 5 = 0
## 2 3 5 = 0
## 1 2 4 = 0
```

See how the result is equal to the original \(k\)-form \(2dy_2\wedge dy_4\wedge dy_5\), except for some numerical roundoff error. In this case the extra rows have values small enough for the print method to print an exact zero, but not sufficiently small to be eliminated by the `spray`

internals. The `spray`

package version 1.0-8 does not have the capability of removing small entries (this will be available in 1.0-9); but in the meantime we can do this:

`as.kform(index(o),zapsmall(value(o)))`

```
## val
## 2 4 5 = 2
```

returning us to the original value of `o`

.

Given a \(k\)-form \(\omega\), Spivak defines the differential of \(\omega\) to be a \((k+1)\)-form \(d\omega\) as follows. If

\[ \omega = \sum_{ i_1 < i_2 <\cdots<i_k} \omega_{i_1i_2\ldots i_k} dx^{i_1}\wedge dx^{i_2}\wedge\cdots\wedge dx^{i_k} \]

then

\[ d\omega = \sum_{ i_1 < i_2 <\cdots<i_k} \sum_{\alpha=1}^n D_\alpha\left(\omega_{i_1i_2\ldots i_k}\right) \cdot dx^{i_1}\wedge dx^{i_2}\wedge\cdots\wedge dx^{i_k} \]

Hubbard and Hubbard define the exterior derivative \(d\phi\) (they use a bold font, \(\mathbf{d}\phi\)) of the \(k\)-form \(\phi\) as the \((k+1)\)-form given by

\[ {d}\phi \left({v}_i,\ldots,{v}_{k+1}\right) = \lim_{h\longrightarrow 0}\frac{1}{h^{k+1}}\int_{\partial P_{x}\left(h{v}_1,\ldots,h{v}_{k+1}\right)}\phi \]

which, by their own account, is a rather opaque mathematical idiom. However, the definition makes sense and it is consistent with Spivak’s definition above. The definition allows one to express the fundamental theorem of calculus in an arbitrary number of dimensions without modification.

It can be shown that

\[ {d}\left(f\,dx_{i_1}\wedge\cdots\wedge dx_{i_k}\right)= {d}f\wedge dx_{i_1}\wedge\cdots\wedge dx_{i_k} \]

where \(f\colon\mathbb{R}^n\longrightarrow\mathbb{R}\) is a scalar function of position. The package provides `grad()`

, which when given a vector \(x_1,\ldots,x_n\) returns the one-form

\[ \sum_{i=1}^n x_idx_i \]

This is useful because \(df=\sum_{j=1}^n\left(D_j f\right)\,dx_j\). Thus

`grad(c(0.4,0.1,-3.2,1.5))`

```
## val
## 3 = -3.2
## 4 = 1.5
## 2 = 0.1
## 1 = 0.4
```

We will use the `grad()`

function to verify that, in \(\mathbb{R}^n\), a certain \((k-1)\)-form has zero work function. Motivated by the fact that

\[ F_3=\frac{1}{\left(x^2+y^2+z^2\right)^{3/2}} \begin{pmatrix}x\\y\\z\end{pmatrix} \]

is a divergenceless velocity field in \(\mathbb{R}^3\), H&H go on to define [page 548, equation 6.7.16]

\[ \omega_n= d\frac{1}{\left(x_1^2+\ldots +x_n^2\right)^{n/2}}\sum_{i=1}^{n}(-1)^{i-1} x_idx_1\wedge\cdots\wedge\widehat{dx_i}\wedge\cdots\wedge dx_n \]

(where a hat indicates the absence of a term), and show analyically that \(d\omega=0\). Here I show this using R idiom. The first thing is to define a function that implements the hat:

```
f <- function(x){
n <- length(x)
as.kform(t(apply(diag(n)<1,2,which)))
}
```

So, for example:

`f(1:5)`

```
## val
## 1 2 4 5 = 1
## 1 2 3 5 = 1
## 1 3 4 5 = 1
## 1 2 3 4 = 1
## 2 3 4 5 = 1
```

Then we can use the `grad()`

function to calculate \(d\omega\), using the quotient law to express the derivatives analytically:

```
df <- function(x){
n <- length(x)
S <- sum(x^2)
grad(rep(c(1,-1),length=n)*(S^(n/2) - n*x^2*S^(n/2-1))/S^n
)
}
```

Thus

`df(1:5)`

```
## val
## 4 = 2.03e-05
## 5 = -5.67e-05
## 3 = 8.10e-06
## 2 = -2.84e-05
## 1 = 4.05e-05
```

Now we can use the wedge product of the two parts to show that the exterior derivative is zero:

```
x <- rnorm(9)
print(df(x) %^% f(x)) # should be zero
```

```
## val
## 1 2 3 4 5 6 7 8 9 = 0
```

```
## val
## 1 2 3 4 5 6 7 8 9 = 0
```

We can use the package to verify the celebrated fact that, for any \(k\)-form \(\phi\), \(d\left(d\phi\right)=0\). The first step is to define scalar functions `f1(), f2(), f3()`

, all \(0\)-forms:

```
f1 <- function(w,x,y,z){x + y^3 + x*y*w*z}
f2 <- function(w,x,y,z){w^2*x*y*z + sin(w) + w+z}
f3 <- function(w,x,y,z){w*x*y*z + sin(x) + cos(w)}
```

Now we need to define elementary \(1\)-forms:

```
dw <- as.kform(1)
dx <- as.kform(2)
dy <- as.kform(3)
dz <- as.kform(4)
```

I will demonstrate the theorem by defining a \(2\)-form which is the sum of three elementary two-forms, evaluated at a particular point in \(\mathbb{R}^4\):

```
phi <-
(
+f1(1,2,3,4) %^% dw %^% dx
+f2(1,2,3,4) %^% dw %^% dy
+f3(1,2,3,4) %^% dy %^% dz
)
```

We could use slightly slicker R idiom by defining elementary forms `e1,e2,e3`

and then defining `phi`

to be a linear sum, weighted with \(0\)-forms given by the (scalar) functions `f1,f2,f3`

:

```
e1 <- dw %^% dx
e2 <- dw %^% dy
e3 <- dy %^% dz
phi <-
(
+f1(1,2,3,4) %^% e1
+f2(1,2,3,4) %^% e2
+f3(1,2,3,4) %^% e3
)
phi
```

```
## val
## 3 4 = 25.44960
## 1 3 = 29.84147
## 1 2 = 53.00000
```

Now to evaluate first derivatives of `f1()`

etc at point \((1,2,3,4)\), using `Deriv()`

from the Deriv package:

```
library("Deriv")
Df1 <- Deriv(f1)(1,2,3,4)
Df2 <- Deriv(f2)(1,2,3,4)
Df3 <- Deriv(f3)(1,2,3,4)
```

So `Df1`

etc are numeric vectors of length 4, for example:

`Df1`

```
## w x y z
## 24 13 35 6
```

To calculate `dphi`

, or \(d\phi\), we can use function `grad()`

:

```
dphi <-
(
+grad(Df1) %^% e1
+grad(Df2) %^% e2
+grad(Df3) %^% e3
)
dphi
```

```
## val
## 1 2 3 = 23.00000
## 1 3 4 = 30.15853
## 2 3 4 = 11.58385
## 1 2 4 = 6.00000
```

Now work on the differential of the differential. First evaluate the Hessians (4x4 numeric matrices) at the same point:

```
Hf1 <- matrix(Deriv(f1,nderiv=2)(1,2,3,4),4,4)
Hf2 <- matrix(Deriv(f2,nderiv=2)(1,2,3,4),4,4)
Hf3 <- matrix(Deriv(f3,nderiv=2)(1,2,3,4),4,4)
```

For example

`Hf1`

```
## w x y z
## w 0 12 8 6
## x 12 0 4 3
## y 8 4 18 2
## z 6 3 2 0
```

(note the matrix is symmetric; also note carefully the nonzero diagonal term). But \(dd\phi\) is clearly zero as the Hessians are symmetrical:

```
ddphi <- # should be zero
(
+as.kform(which(!is.na(Hf1),arr.ind=TRUE),c(Hf1))
+as.kform(which(!is.na(Hf2),arr.ind=TRUE),c(Hf2))
+as.kform(which(!is.na(Hf3),arr.ind=TRUE),c(Hf3))
)
ddphi
```

`## empty sparse array with 2 columns`

as expected.

In its most general form, Stokes’s theorem states

\[ \int_{\partial X}\phi=\int_Xd\phi \]

where \(X\subset\mathbb{R}^n\) is a compact oriented \((k+1)\)-dimensional manifold with boundary \(\partial X\) and \(\phi\) is a \(k\)-form defined on a neighborhood of \(X\).

We will verify Stokes, following 6.9.5 of Hubbard in which

\[ \phi= \left(x_1-x_2^2+x_3^3-\cdots\pm x_n^n\right) \left( \sum_{i=1}^n dx_1\wedge\cdots\wedge\widehat{dx_i}\wedge\cdots\wedge dx_n \right) \]

(a hat indicates that a term is absent), and we wish to evaluate \(\int_{\partial C_a}\phi\) where \(C_a\) is the cube \(0\leq x_j\leq a, 1\leq j\leq n\). Stokes tells us that this is equal to \(\int_{C_a} d\phi\), which is given by

\[ d\phi = \left( 1+2x_2+\cdots + nx_n^{n-1}\right) dx_1\wedge\cdots\wedge dx_n \]

and so the volume integral is just

\[ \sum_{j=1}^n \int_{x_1=0}^a \int_{x_2=0}^a \cdots \int_{x_i=0}^a jx_j^{j-1} dx_1 dx_2\ldots dx_n= a^{n-1}\left(a+a^2+\cdots+a^n\right). \]

Stokes’s theorem, being trivial, is not amenable to direct numerical verification but the package does allow slick creation of \(\phi\):

```
phi <- function(x){
n <- length(x)
sum(x^seq_len(n)*rep_len(c(1,-1),n)) * as.kform(t(apply(diag(n)<1,2,which)))
}
phi(1:9)
```

```
## val
## 1 2 4 5 6 7 8 9 = 371423053
## 1 2 3 4 6 7 8 9 = 371423053
## 1 2 3 4 5 6 7 9 = 371423053
## 1 2 3 4 5 6 8 9 = 371423053
## 1 3 4 5 6 7 8 9 = 371423053
## 1 2 3 4 5 6 7 8 = 371423053
## 2 3 4 5 6 7 8 9 = 371423053
## 1 2 3 4 5 7 8 9 = 371423053
## 1 2 3 5 6 7 8 9 = 371423053
```

(recall that `phi`

is a function that maps \(\mathbb{R}^9\) to 8-forms. Here we choose \(\left(1,2,\ldots,9\right)\in\mathbb{R}^9\) and `phi(1:9)`

as shown above is the resulting 8-form. Thus, if we write \(\phi_{1:9}\) for `phi(1:9)`

we would have \(\phi_{1:9}\colon\left(\mathbb{R}^9\right)^8\longrightarrow\mathbb{R}\), with package idiom as follows:

```
f <- as.function(phi(1:9))
E <- matrix(runif(72),9,8) # (R^9)^8
f(E)
```

`## [1] 4919044`

Further, \(d\phi\) is given by

```
dphi <- function(x){
nn <- seq_along(x)
sum(nn*x^(nn-1)) * as.kform(seq_along(x))
}
dphi(1:9)
```

```
## val
## 1 2 3 4 5 6 7 8 9 = 405071317
```

(observe that `dphi(1:9)`

is a 9-form, with \(d\phi_{1:9}\colon\left(\mathbb{R}^9\right)^9\longrightarrow\mathbb{R}\)). Numerically:

```
f <- as.function(dphi(1:9))
f(diag(9))
```

`## [1] 405071317`

```
E <- matrix(runif(81),9,9)
f(E)
```

`## [1] -3657970`

`det(E)*f(diag(9)) # should match f(E)`

`## [1] -3657970`

- J. H. Hubbard and B. B. Hubbard 2015.
*Vector calculus, linear algebra and differential forms: a unified aproach*, Ithaca, NY. - M. Spivak 1971.
*Calculus on manifolds*, Addison-Wesley.