## Saturday, 19 October 2019

### VBA error 6219. One small step to the Christoffel symbol

I am trying to write a Word macro to expand the Christoffel symbol automatically. The expansion is simple:$$\Gamma_{\mu\nu}^\sigma=\frac{1}{2}g^{\sigma\rho}\left(\partial_\mu g_{\nu\rho}+\partial_\nu g_{\rho\mu}-\partial_\rho g_{\mu\nu}\right)$$One replaces the Christoffel symbol $\Gamma_{\mu\nu}^\sigma$ by a half times the inverse metric $g^{\sigma\rho}$ times the sum of slightly different index combinations of the partial derivative $\partial_\mu$ of the metric $g_{\nu\rho}$. A new dummy index, $\rho$ in this case, is introduced (it is summed over) and the original indices $\sigma,\mu,\nu$ are placed carefully in the expansion. The Christoffel symbol occurs frequently in General Relativity and once I have done this a few times with different indices my eyes start to pop out, thus the motivation to write a macro to save said eyes.

The macro should be fairly straightforward. We want it to replace something like$$A\Gamma_{\mu\nu}^\sigma X$$by$$A\frac{1}{2}g^{\sigma\rho}\left(\partial_\mu g_{\nu\rho}+\partial_\nu g_{\rho\mu}-\partial_\rho g_{\mu\nu}\right)X$$An equation is an OMath object which consists of OMathFunction objects. So each of $A,\Gamma_{\mu\nu}^\sigma,X$ in the first equation is an  OMathFunction object. In theory it should be easy to replace the OMathFunction object $\Gamma_{\mu\nu}^\sigma$ by a bunch of new OMathFunction objects $\frac{1}{2},g^{\sigma\rho},+\ etc$. Dream on!

When you want to add a new OMathFunction object, you need to call Add method of an OMathFunctions object. The OMathFunctions object is the list of OMathFunction objects in the equation. The second parameter of this Add method is the type of new OMathFunction to add. (This not well described in the documentation.) The list of types is here. So the fraction $\frac{1}{2}$ is an wdOMathFunctionFrac, the inverse metric which has superscripts $g^{\sigma\rho}$ is a wdOMathFunctionScrSup and $+$ is a wdOMathFunctionText. You can see all these in the debugger if you have a look at the OMathFunctions object. This Add method crashes with error 6219 if the type parameter is wdOMathFunctionText, so it is very difficult to get the $+,-$ signs into expansion. The only way I could discover was by using
Selection.TypeText ("+")
I could then insert $g_{\mu\nu}+g^{\mu\nu}$ at the selection point in an equation with this code:
Sub ExampleWriteExpression()
'insertion point should be in equation. Metric + inverse metric inserted
Dim Equation As OMath
Dim MathTerm As OMathFunction

If Selection.OMaths.Count <> 1 Then ExpanderFatalError ("Cursor must be in an equation.")
Set Equation = Selection.OMaths(1)
MathTerm.ScrSub.E.Range = "g"
MathTerm.ScrSub.Sub.Range = ChrW(&H3BC) & ChrW(&H3BD)

Selection.TypeText ("+")

'Set MathTerm = Equation.Functions.Add(Selection.Range, wdOMathFunctionText) still gets error

MathTerm.ScrSup.E.Range = "g"
MathTerm.ScrSup.Sup.Range = ChrW(&H3BC) & ChrW(&H3BD)
End Sub
There was quite a bit of difficulty in getting the selection in the right place and I kept getting things like$$\frac{1}{2}g^{\sigma\rho}\left(\partial_\mu g_{\nu\rho}\partial_\nu g_{\rho\mu}\partial_\rho g_{\mu\nu}\right)+-$$It is necessary align the selection range with the equation range and this is not the simple matter of subtraction that you might expect! It now works  properly and replaces multiple Christoffel symbols in an equation correctly. Click Read more below if you would like a look or copy. Covariant derivatives, Riemann tensors, using, metric and coordinates coming soon!

jpl on msofficeforums corrected me and showed me how to avoid using the Selection which is a bodge. I further refined his technique. It is now very beautiful.😅
'Macros to expand Christoffel symbols and more in MS equations in Office 365 (2019)
'By George Keeling and on my blog at www.general-relativity.net under Tools
'https://www.general-relativity.net/search/label/Tools
'Feel free to use, copy, modify and give away but not for commerce. Please credit me and the website.

'Bugs
'1) does not work with on non-italic indices
Option Explicit
Const gCapGamma = 915               'hex 393
Const gPartialDerivative = 8706     'hex 2202
Const gNabla = 8711
Const gMagic1 = -10187

Dim gIndexesUsed(1 To 51) As Integer  'Count of usage of each index alpha at 1, omega at 25, a at 26, z at 51
Dim gGreekUsed As Boolean

Sub ExpandSymbols()
'Expand all various symbols in an equation
Dim Equation As OMath
Dim MathTerm As OMathFunction, GammaTerm As OMathScrSubSup
Call ZapIndexesUsed
If Selection.OMaths.Count <> 1 Then
ExpanderFatalError ("Cursor must be in an equation.")
End If
Set Equation = Selection.OMaths(1)
Call FindIndexesUsed(Equation)
Call ExpandEquation(Equation)
End Sub

Sub ExpandEquation(ByVal Equation As OMath)
Dim MathTerm As OMathFunction, CovariantDerivativeTerm As OMathFunction
Dim InnerEquation As OMath
Dim iInnerEq As Integer

Set CovariantDerivativeTerm = Nothing

For Each MathTerm In Equation.Functions
If CovariantDerivativeTerm Is Nothing Then
If (MathTerm.Type = wdOMathFunctionScrSubSup) Then
If Len(MathTerm.Range.Text) >= 2 Then
If (AscW(Mid(MathTerm.Range.Text, 2, 1)) = gCapGamma) Then 'Have Christoffel symbol.
Call ExpandChristoffel(Equation.Functions, MathTerm)
End If
End If
ElseIf MathTerm.Type = wdOMathFunctionScrSub Then
If Len(MathTerm.Range.Text) >= 2 Then
If (AscW(Mid(MathTerm.Range.Text, 2, 1)) = gNabla) Then 'Nabla: Covariant derivative
Set CovariantDerivativeTerm = MathTerm
'and continue to the next term which is what we must operate on
End If
End If
ElseIf MathTerm.Type = wdOMathFunctionDelim Then   'delimiter - brackets, need to recurse
For iInnerEq = 1 To MathTerm.Delim.E.Count
Set InnerEquation = MathTerm.Delim.E.Item(iInnerEq)
Call ExpandEquation(InnerEquation)
Next
End If
Else
Call ExpandCovariantDerivative(Equation.Functions, CovariantDerivativeTerm, MathTerm)
Set CovariantDerivativeTerm = Nothing
End If
Next
End Sub

Sub ExpandChristoffel(Functions As OMathFunctions, MathTerm As OMathFunction)
'Expand Christoffel symbol
'Functions are the functions of the original equation, which we add to
'Mathterm is the Christoffel symbol term
Dim UpIndex As Integer, LeftIndex As Integer, RightIndex As Integer, DummyIndex As Integer
Dim NewMathTerm As OMathFunction
Dim BracketFunctions As OMathFunctions, BracketTerm As OMathFunction    'just like paramters of this function
Dim InsertionPoint As Range

UpIndex = ToUnicode(Mid(MathTerm.ScrSubSup.Sup.Range.Text, 2, 1))
LeftIndex = ToUnicode(Mid(MathTerm.ScrSubSup.Sub.Range.Text, 2, 1))
RightIndex = ToUnicode(Mid(MathTerm.ScrSubSup.Sub.Range.Text, 4, 1))
DummyIndex = GetNewDummyIndex()

MathTerm.Range.Text = ""    'removes Christoffel symbol
Set InsertionPoint = MathTerm.Range

'1/2
Set NewMathTerm = InsertFunction(Functions, InsertionPoint, wdOMathFunctionFrac)
NewMathTerm.Frac.Num.Range.Text = "1"
NewMathTerm.Frac.Den.Range.Text = "2"
NewMathTerm.Frac.Type = wdOMathFracBar

'inverse metric
Set NewMathTerm = InsertFunction(Functions, InsertionPoint, wdOMathFunctionScrSup)
NewMathTerm.ScrSup.E.Range = "g"
NewMathTerm.ScrSup.Sup.Range = ChrW(UpIndex) & ChrW(DummyIndex)

'brackets (defaults to round brackets)
Set NewMathTerm = InsertFunction(Functions, InsertionPoint, wdOMathFunctionDelim)
Set BracketFunctions = NewMathTerm.Delim.E(1).Functions
Set BracketTerm = BracketFunctions(1)

InsertionPoint.Start = InsertionPoint.Start - 1       'get into the bracket
InsertionPoint.Collapse (wdCollapseStart)

'first + term
Set NewMathTerm = InsertFunction(BracketFunctions, InsertionPoint, wdOMathFunctionScrSub)
NewMathTerm.ScrSub.E.Range = ChrW(gPartialDerivative)
NewMathTerm.ScrSub.Sub.Range = ChrW(LeftIndex)
Set NewMathTerm = InsertFunction(BracketFunctions, InsertionPoint, wdOMathFunctionScrSub)
NewMathTerm.ScrSub.E.Range = "g"
NewMathTerm.ScrSub.Sub.Range = ChrW(DummyIndex) & ChrW(RightIndex)

'second + term
Set InsertionPoint = BracketTerm.Range
Call InsertText(InsertionPoint, "+")
Set NewMathTerm = InsertFunction(BracketFunctions, InsertionPoint, wdOMathFunctionScrSub)
NewMathTerm.ScrSub.E.Range = ChrW(gPartialDerivative)
NewMathTerm.ScrSub.Sub.Range = ChrW(RightIndex)
Set NewMathTerm = InsertFunction(BracketFunctions, InsertionPoint, wdOMathFunctionScrSub)
NewMathTerm.ScrSub.E.Range = "g"
NewMathTerm.ScrSub.Sub.Range = ChrW(LeftIndex) & ChrW(DummyIndex)

'third - term
Call InsertText(InsertionPoint, "-")
Set NewMathTerm = InsertFunction(BracketFunctions, InsertionPoint, wdOMathFunctionScrSub)
NewMathTerm.ScrSub.E.Range = ChrW(gPartialDerivative)
NewMathTerm.ScrSub.Sub.Range = ChrW(DummyIndex)
Set NewMathTerm = InsertFunction(BracketFunctions, InsertionPoint, wdOMathFunctionScrSub)
NewMathTerm.ScrSub.E.Range = "g"
NewMathTerm.ScrSub.Sub.Range = ChrW(LeftIndex) & ChrW(RightIndex)

End Sub

Sub ExpandCovariantDerivative(Functions As OMathFunctions, CdTerm As OMathFunction, MathTerm As OMathFunction)
'Expand Covariant Derivative
'Functions are the functions of the original equation, which we add to
'CdTerm is the covariant derivative operator
'Mathterm is the term after the covariant derivative term, the operand, a tensor with up or down indices or both
'We do not (yet) do an operand in brackets or deal with metric compatibility

'first to all up indices, then all down indices
Dim UpIndexes(1 To 51) As Integer
Dim DownIndexes(1 To 51) As Integer
Dim UpIndexString As String, DownIndexString As String, Tensor As String, DummyIndex As Integer, CdIndex As Integer
Dim NewMathTerm As OMathFunction

If MathTerm.Type = wdOMathFunctionScrSub Then
DownIndexString = MathTerm.ScrSub.Sub.Range.Text
Tensor = ToUnicode(Mid(MathTerm.ScrSub.E.Range.Text, 2, 1))
ElseIf MathTerm.Type = wdOMathFunctionScrSup Then
UpIndexString = MathTerm.ScrSup.Sup.Range.Text
Tensor = ToUnicode(Mid(MathTerm.ScrSup.E.Range.Text, 2, 1))
ElseIf MathTerm.Type = wdOMathFunctionScrSubSup Then
DownIndexString = MathTerm.ScrSubSup.Sub.Range.Text
UpIndexString = MathTerm.ScrSubSup.Sup.Range.Text
Tensor = ToUnicode(Mid(MathTerm.ScrSubSup.E.Range.Text, 2, 1))
Else
Exit Sub        'cannot recognise operand ***************************************
End If

DummyIndex = GetNewDummyIndex()
CdIndex = ToUnicode(Mid(CdTerm.ScrSub.Sub.Range.Text, 2, 1))

CdTerm.ScrSub.E.Range.Text = ChrW(gPartialDerivative)    'change nabla to partial derivative

'Now add + term for each up index and - term for each down index
Selection.TypeText ("+" & Tensor)

End Sub

Sub ZapIndexesUsed()
'values preserved in gIndexesUsed from one call to another ...
Dim iIU As Integer
For iIU = 1 To UBound(gIndexesUsed)
gIndexesUsed(iIU) = 0
Next
gGreekUsed = False
End Sub

Sub FindIndexesUsed(ByVal Equation As OMath)
'Find all tensor indexes used in equation, so that we do not use them as dummy variables.
'only search for lower case italic roman and greek!
Dim MathTerm As OMathFunction
Dim InnerEquation As OMath
Dim iInnerEq As Integer

For Each MathTerm In Equation.Functions
If MathTerm.Type = wdOMathFunctionScrSubSup Then   'super and sub script
Call CheckIndexes(MathTerm.ScrSubSup.Sub.Range.Text)
Call CheckIndexes(MathTerm.ScrSubSup.Sup.Range.Text)
End If
If MathTerm.Type = wdOMathFunctionScrSup Then   'super script
Call CheckIndexes(MathTerm.ScrSup.Sup.Range.Text)
End If
If MathTerm.Type = wdOMathFunctionScrSub Then   'super script
Call CheckIndexes(MathTerm.ScrSub.Sub.Range.Text)
End If
If MathTerm.Type = wdOMathFunctionDelim Then   'delimiter - brackets, need to recurse
For iInnerEq = 1 To MathTerm.Delim.E.Count
Set InnerEquation = MathTerm.Delim.E.Item(iInnerEq)
Call FindIndexesUsed(InnerEquation)
Next
End If
Next
End Sub

Sub CheckIndexes(IndexText As String)
'Check all the indexes in a string which may contain spaces.
Dim iText As Integer, UniCode As Integer
For iText = 1 To Len(IndexText)
UniCode = 0
UniCode = ToUnicode(Mid(IndexText, iText, 1))
If UniCode = &H3D5 Then
UniCode = &H3C6         'alternate phi, same as phi
End If
'Now have correct Unicode char in UniCode which we will use to index into gIndexesUsed
If (UniCode > 0) Then
If (UniCode > 0) And (UniCode < &H7B) Then
'It's roman
UniCode = UniCode - &H60 + 25
Else
UniCode = UniCode - &H3B0
gGreekUsed = True
End If
gIndexesUsed(UniCode) = gIndexesUsed(UniCode) + 1
End If
Next
End Sub

Function ToUnicode(Char As String) As Integer
'convert one charachter string from equation into unicode. 0 if unknown.
'Charachter is lower case roman or greek italic.
'includes alternate theta, upsilon, phi
Dim CharCode As Integer
CharCode = AscW(Char)
If (CharCode >= -8452) And (CharCode <= -8423) Then
ToUnicode = 9397 + CharCode       'Unicode greek
ElseIf CharCode = 8462 Then
ToUnicode = &H68                'roman h is special. Planck at work!
ElseIf (CharCode >= -9138) And (CharCode <= -9113) Then
ToUnicode = 9235 + CharCode       'Unicode roman
Else
ToUnicode = 0
End If
End Function

Function GetNewDummyIndex() As Integer
'get new dummy index.  greek if any greek in list, otherwise roman. Start with mu,nu,rho, sigma,tau,lambda, kappa
Dim iIU As Integer
If gGreekUsed Then
If gIndexesUsed(12) = 0 Then
gIndexesUsed(12) = 1
GetNewDummyIndex = &H3BC
Exit Function
End If
If gIndexesUsed(13) = 0 Then
gIndexesUsed(13) = 1
GetNewDummyIndex = &H3BD
Exit Function
End If
If gIndexesUsed(17) = 0 Then
gIndexesUsed(17) = 1
GetNewDummyIndex = &H3C1
Exit Function
End If
If gIndexesUsed(19) = 0 Then
gIndexesUsed(19) = 1
GetNewDummyIndex = &H3C3
Exit Function
End If
If gIndexesUsed(20) = 0 Then
gIndexesUsed(20) = 1
GetNewDummyIndex = &H3C4
Exit Function
End If
If gIndexesUsed(11) = 0 Then
gIndexesUsed(11) = 1
GetNewDummyIndex = &H3BB
Exit Function
End If
If gIndexesUsed(10) = 0 Then
gIndexesUsed(10) = 1
GetNewDummyIndex = &H3BA
Exit Function
End If
'No obvious spare ones found. Start at alpha and at z (oops, in worst case could be roman)
For iIU = 1 To UBound(gIndexesUsed)
If gIndexesUsed(iIU) = 0 Then
gIndexesUsed(iIU) = 1
If iIU <= 25 Then
GetNewDummyIndex = iIU + &H3B0      'greek
Else
GetNewDummyIndex = iIU + &H60      'bad luck roman
End If
Exit Function
End If
Next
Else
'roman just do a to z then alpha to omega
For iIU = 26 To 51
If gIndexesUsed(iIU) = 0 Then
gIndexesUsed(iIU) = 1
GetNewDummyIndex = iIU + &H60 - 25
Exit Function
End If
Next
For iIU = 1 To 25
If gIndexesUsed(iIU) = 0 Then
gIndexesUsed(iIU) = 1
GetNewDummyIndex = iIU + &H3B0
Exit Function
End If
Next
End If
'very bad luck, no indices left
GetNewDummyIndex = &H2605  'black star
End Function

Function InsertFunction(Functions As OMathFunctions, InsertionPoint As Range, FuncType As WdOMathFunctionType) As OMathFunction
'Add a function in Functions at InsertionPoint and move InsertionPoint to after the function, ready for next
Dim NewFunction As OMathFunction
Set InsertionPoint = NewFunction.Range
InsertionPoint.Collapse (wdCollapseEnd)
Set InsertFunction = NewFunction
End Function

Sub InsertText(InsertionPoint As Range, MyText As String)
'Inserts MyText at InsertionPoint and returns moves InsertionPoint to after that text
'so this is very similar to
'Equation.Functions.Add(InsertionPoint, wdOMathFunctionNormalText) .... which does not work!!
'many thankss to jpl for this. https://www.msofficeforums.com/word-vba/31587-vba-omath-object.html
InsertionPoint.Text = MyText
InsertionPoint.Collapse (wdCollapseEnd)
End Sub

Sub ExpanderFatalError(Message As String)
Call MsgBox(Message, vbOKOnly, "Expander says")
End
End Sub

'******************************** test functions below here
Sub junk()
End Sub

Sub ExampleWriteSymbols()
'Insert capital gamma, partial derivative, nabla
Call Selection.InsertSymbol(gCapGamma, , True)
Call Selection.InsertSymbol(gPartialDerivative, , True)
Call Selection.InsertSymbol(gNabla, , True)
End Sub

Sub ExampleWriteAlphabet()
'insertion point should be in equation. alphabet written one charachter before end.
Dim LetterCode As Integer, Letter0 As Integer

Dim Equation As OMath
Dim MathTerm As OMathFunction, GammaTerm As OMathScrSubSup
Dim MyRange As Range

Letter0 = &H61 ' Start letter = 391 ALPHA, 3B1 alpha, 41 A, 61 a

If Selection.OMaths.Count <> 1 Then
ExpanderFatalError ("Cursor must be in an equation.")
End If
Set Equation = Selection.OMaths(1)
Set MyRange = Equation.Range
MyRange.Collapse (wdCollapseEnd)
Call MyRange.MoveEnd(wdCharacter, -1)
MyRange.Text = ""
For LetterCode = Letter0 To Letter0 + 20
MyRange.Text = MyRange.Text & ChrW(LetterCode)
Next
End Sub

Sub ExampleWriteExpression()
'insertion point should be in equation. Metric + inverse metric inserted
Dim Equation As OMath
Dim MathTerm As OMathFunction
Dim InsertionPoint As Range

If Selection.OMaths.Count <> 1 Then ExpanderFatalError ("Cursor must be in an equation.")
Set Equation = Selection.OMaths(1)
Set InsertionPoint = Selection.Range

Set MathTerm = InsertFunction(Equation.Functions, InsertionPoint, wdOMathFunctionScrSub)
MathTerm.ScrSub.E.Range = "g"
MathTerm.ScrSub.Sub.Range = ChrW(&H3BC) & ChrW(&H3BD)

Call InsertText(InsertionPoint, "+")

Set MathTerm = InsertFunction(Equation.Functions, InsertionPoint, wdOMathFunctionScrSup)
MathTerm.ScrSup.E.Range = "g"
MathTerm.ScrSup.Sup.Range = ChrW(&H3BC) & ChrW(&H3BD)
End Sub

Dim Equation As OMath
Dim MathTerm As OMathFunction
Dim Symbol(1 To 6) As Integer

If Selection.OMaths.Count <> 1 Then
ExpanderFatalError ("Cursor must be in equation.")
End If
Set Equation = Selection.OMaths(1)

For Each MathTerm In Equation.Functions
Symbol(1) = AscW(Mid(MathTerm.Range.Text, 1, 1))
Symbol(2) = AscW(Mid(MathTerm.Range.Text, 2, 1))
Symbol(3) = AscW(Mid(MathTerm.Range.Text, 3, 1))
Symbol(4) = AscW(Mid(MathTerm.Range.Text, 4, 1))
Symbol(5) = AscW(Mid(MathTerm.Range.Text, 5, 1))
Symbol(6) = AscW(Mid(MathTerm.Range.Text, 6, 1))
Next
End Sub 'break here to check results