 Búsqueda de Soluciones

 INF-285 Computación Científica

 Autor: Francisco Andrades

Lenguaje: Python

Temas:

    - Bisección
    - Punto Fijo

### Polinomios de Legendre
Dada la función compuesta $f$
\begin{equation}
f_{n,m}(x) = L_n(C_m(x)),
\end{equation}
donde $L_n$ es conocido como el polinomio de Legendre de grado $n$ definido de la siguiente forma:
\begin{equation}
L_{n}(x)=\frac{1}{2^{n}} \sum_{k=0}^{n}\left(\begin{array}{l}
n \\
k
\end{array}\right)^{2}(x-1)^{n-k}(x+1)^{k},
\end{equation}
y $C_m$ es el polinomio de Chebyshev
\begin{equation}
C_m(x) = \cos(m \cdot \arccos(x)).
\end{equation}

Utilizando el método de Bisección y Punto fijo se pide obtener la raíz de $f$ más cercana a $0.5$ dado un valor de $m$ y $n$

*Hint: Las raíces de Legendre son conocidas*

#### Resolución

##### Construir el polinomio

El primer paso será construir la función compuesta $f$
\begin{equation}
f_{n,m}(x) = L_n(C_m(x))
\end{equation}

```python

"""Importante, inicialmente construí la función utilizando las herramientas que
entrega np (np.legendre(), np.chevit()), sin embargo esta implementación tenía problemas para n y m lo suficientemente grandes
por lo que decidí implementarlo yo mismo.
"""
def construir_polinomio(n,m,x):
    numero = (1/2**n)
    sum = 0
    for k in range(n+1):
        sum += (scipy.special.binom(n,k)**2)*((np.cos(m*np.arccos(x))-1)**(n-k))*((np.cos(m*np.arccos(x))+1)**k)
    return numero*sum

def f(n,m):
    r = lambda x: construir_polinomio(n,m,x)
    return r

f1 = f(2,1)
x = np.arange(-1, 1, 0.0001).tolist()

pyplot.plot(x, [f1(i) for i in x])
pyplot.title("Para n = 2, m = 1")
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-5, 5)
pyplot.ylim(-5, 5)
pyplot.show()
```

Se puede apreciar que tanto el dominio como el recorrido de la función pertenecen al intervalo *[-1,1]*.

Se graficará la función con distintos n y m, para poder hacer un análisis visual.

```python

f1 = f(2,1)
f2 = f(3,3)
f3 = f(4,6)
f4 = f(10,8)
x = np.arange(-1, 1, 0.0001).tolist()

pyplot.plot(x, [f1(i) for i in x])
pyplot.title("Para n = 2, m = 1")
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

pyplot.plot(x, [f2(i) for i in x])
pyplot.title("Para n = 3, m = 3")
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

pyplot.plot(x, [f3(i) for i in x])
pyplot.title("Para n = 4, m = 6")
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

pyplot.plot(x, [f4(i) for i in x])
pyplot.title("Para n = 10, m = 8")
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

```

##### Inferencias importantes

Mediante un análisis puramente visual, se puede inferir:

    1. La función tiene n*m soluciones
    2. La función es simétrica o antisimétrica dependiendo de los valores de n y m
    3. De la conclusión anterior se puede inferir que existen la misma cantidad de soluciones en [0,1] y [-1,0]
    4. Las soluciones no son equidistantes, se van haciendo más frecuentes cuando x se acerca a 1 o -1
    5. Siempre va a existir una solución en el intervalo [0,1]

Debido a que el problema consiste en encontrar la solución más cercana a 0.5, se continuará trabajando sin considerar todas las soluciones para x < 0.

##### Método de Bisección

El método de bisección tiene una complicación, requiere de un intervalo que contenga una única solución.

Si se intentara resolver el problema utilizando bisección sobre el intervalo *[0,1]*, por ejemplo, el método fallaría debido a la cantidad de soluciones presentes. Además, debido a la naturaleza del problema, deberemos encontrar un intervalo que contenga la solución (y solo ésta solución) más próxima a 0.5.

Pero, ¿cómo encontrar dicho intervalo?.

Ya se ha señalado que la cantidad de soluciones y el valor de éstas dependen de *n* y *m*, por lo que el intervalo de interés cambia si *n* o *m* cambian. Además las soluciones no son siquiera equidistantes, por lo que no existe una distancia *k* = xi - x(i+1) que permita inferir donde se encuentra el intervalo.

En esta tarea se propondrá un método que busca resolver este problema.

###### Un análisis estadístico

Volvamos a ver la función.

```python
x = np.arange(0, 1, 0.0001).tolist()

pyplot.plot(x, [f4(i) for i in x])
pyplot.title("Para n = 10, m = 8")
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(0, 1)
pyplot.ylim(-1, 1)
pyplot.axvline(0.25, color="red")
pyplot.axvline(0.75, color="red")
pyplot.axvline(0.5, color="red")
pyplot.show()
```

Definamos el término Densidad de las soluciones Vs como:

\begin{equation}Vs = round(\frac{Cantidad\ de\ soluciones\ en\ un\ intervalo} {longitud\ del\ intervalo*10})\end{equation}

Para el intervalo *[0.25,0.5]*, *Vs = 2*.

Para el intervalo *[0.5,0.75]*, *Vs = 3*

Así, Vs aumenta a medida que aumenta x.

Si bien las soluciones no son equidistantes, quizás Vs aumenta de forma regular.

Observemos como se comporta la función en el intervalo *[0,0.5].

```python
def analisis(f):
    pyplot.plot(x, [f(i) for i in x])
    pyplot.axhline(0, color="black")
    pyplot.axvline(0, color="black")
    # Limitar los valores de los ejes.
    pyplot.xlim(0, 1)
    pyplot.ylim(-1, 1)
    pyplot.axvline(0.25, color="red")
    pyplot.axvline(0.5, color="red")
    pyplot.show()

###probamos de forma experimental, cuantas soluciones hay en ciertos intervalos
    numero = 0
    lista_soluciones = []
    for i in x:
        if f(i) * numero < 0:
            lista_soluciones.append(i)
        numero = f(i)

    lista_025 = []
    lista_05 = []
    lista_125 = []
    lista_375 = []

    for i in lista_soluciones:
        if i < 0.25:
            lista_025.append(i)

    for i in lista_soluciones:
        if i > 0.25 and i < 0.5:
            lista_05.append(i)

    for i in lista_soluciones:
        if i < 0.125:
            lista_125.append(i)

    for i in lista_soluciones:
        if i > 0.25 and i < 0.375:
            lista_375.append(i)

    print("Vs para el intervalo [0,0.5]")
    print(np.round((len(lista_05)+len(lista_025))/5))

    print("Vs para el intervalo [0,1]")
    print(np.round((len(lista_soluciones)/10)))

    print("Vs para el intervalo [0,0.125]")
    print(np.round(len(lista_125)/1.25))

    print("Vs para el intervalo [0.125,0.25]")
    print(np.round((len(lista_025)-len(lista_125))/1.25))

    print("Vs para el intervalo [0.25,0.375]")
    print(np.round(len(lista_375)/1.25))

    print("Vs para el intervalo [0.375,0.5]")
    print(np.round((len(lista_05)-len(lista_375))/1.25))

analisis(f(10,7))
analisis(f(14,20))
analisis(f(17,24))
```

De este análisis se puede extraer lo siguiente para n y m cualquiera:

    1. Vs [0,0.5] / (Vs [0,1]*2) se acerca a 0.3333
    2. Vs [0,0.25] / Vs ([0,0.5]*2) se acerca a 0.48
    3. Vs [0.25,0.5] / Vs ([0,0.5]*2) se acerca a 0.52
    4. Vs se mantiene igual o aumenta en 1 para todo el intervalo [0,0.25]
    5. Vs se mantiene igual o aumenta en 1 para todo el intervalo [0.25,0.5]

**Sobre los puntos 1 ,2 y 3:**

 Para dos intervalos a y b que cumplen:

 \begin{equation}
\frac{Vs(a)}{Vs(b)*2} = c,
\end{equation}

en donde,

\begin{equation}largo(b) = 2largo(a),
\end{equation}

Con Ci = Cantidad de soluciones en i, tenemos :

\begin{equation} \frac{\frac{Ca}{largo(a)}}{ \frac{2Cb}{largo(b)}} = c,\end{equation}

\begin{equation}\frac{Ca * 2largo(a)} {2Cb*largo(a)} = c\end{equation}

\begin{equation}\frac{Ca}{Cb} = c\end{equation}

Esto nos permite asegurar que,

\begin{equation}Ca = c*Cb\end{equation}

**Sobre los puntos 4 y 5:**

Para 2 intervalos a y b pertenecientes a [0,0.25] o [0.25,0.5], tenemos:

\begin{equation}Vs(a) = Vs(b),\end{equation}
Lo que se traduce a

\begin{equation}\frac{Ca}{largo(a)} = \frac{Cb}{largo(b)}, \end{equation}

Finalmente,

\begin{equation}\frac{largo(a)}{largo(b)} = \frac{Ca}{Cb}\end{equation}

Esta igualdad demuestra que la cantidad de soluciones sigue una relación directa con el largo del intervalo.

La mitad de un intervalo, contiene la mitad de soluciones.

    El hecho de que Vs aumente en 1 en algunos casos, simplemente se debe a la existencia de n soluciones, con n impar. En este caso, uno de los intervalos va a contener una solución más. Sin embargo, la relación entre los Vs sigue siendo constante.

    Para n = 3: Una mitad tendrá dos soluciones.

Estas afirmaciones permiten buscar el intervalo de interés dentro de *[0,0.5]* conociendo sólo los valores de n y m.
Así, si n = 5 y m = 6:

     En [0,1] existen 15 soluciones.
     En [0,0.5] existen 5 soluciones.
     En [0,0.25] existen 2 soluciones.
     En [0.25,0.5] existen 3 soluciones.
     ...
     En [0.4375,0.5] existe 1 solución (La más cercana a 0.5).

**Sobre el intervalo *[0.5,1]*:**

Lógicamente existen 2/3 de las soluciones.

En este intervalo Vs aumenta de forma más rápida, por lo que el análisis es un poco más complicado.

```python
def analisis2(f):

    pyplot.plot(x, [f(i) for i in x])
    pyplot.axhline(0, color="black")
    pyplot.axvline(0, color="black")
    # Limitar los valores de los ejes.
    pyplot.xlim(0, 1)
    pyplot.ylim(-1, 1)
    pyplot.axvline(0.75, color="red")
    pyplot.axvline(0.5, color="red")
    pyplot.show()

    numero = 0
    lista_soluciones = []
    for i in x:
        if f(i) * numero < 0:
            lista_soluciones.append(i)
        numero = f(i)

    lista_075 = []
    lista_1 = []
    lista_625 = []
    lista_875= []
    lista_9375 = []
    listax = []

    for i in lista_soluciones:
        if i > 0.5 and i < 0.75:
            lista_075.append(i)

    for i in lista_soluciones:
        if i > 0.75 and i < 1:
            lista_1.append(i)

    for i in lista_soluciones:
        if i > 0.5 and i < 0.625:
            lista_625.append(i)

    for i in lista_soluciones:
        if i > 0.75 and i < 0.875:
            lista_875.append(i)

    for i in lista_soluciones:
        if i > 0.875 and i < 0.9375:
            lista_9375.append(i)

    for i in lista_soluciones:
        if i > 0.875 and i < 1:
            listax.append(i)

    lista_5625 = []
    for i in lista_625:
        if i < 0.5625:
            lista_5625.append(i)

    print("Vs para el intervalo [0.5,0.75]")
    print(np.round(len(lista_075)/2.5))

    print("Vs para el intervalo [0.5,1]")
    print(np.round((len(lista_075)+len(lista_1))/5))

    print("Vs para el intervalo [0.5,0.625]")
    print(len(lista_625)/1.25)

    print("Vs para el intervalo [0.5,0.5625]")
    print(np.round(len(lista_5625)/0.625))

    print("Vs para el intervalo [0.5625,0.625]")
    print(np.round((len(lista_625)-len(lista_5625))/0.625))

analisis2(f(10,7))
analisis2(f(15,20))
analisis2(f(22,15))
```

Con el debido análisis de los Vs, se puede apreciar que:

    1. El intervalo [0.5,0.75] tiene 0.31 de las soluciones del intervalo [0.5,1]
    2. El intervalo [0.5,0.625] tiene 0.47 de las soluciones del intervalo [0.5,0.75]
    3. Dentro del intervalo [0.5,0.625] Vs se mantiene constante.

Para los intervalos [0.75,...] se va a asumir que

\begin{equation}\frac{Vs(a)}{Vs(b)*2} = 0.3 \end{equation}

con
\begin{equation}largo(a) = \frac{largo(b)}{2} \end{equation}

Si bien estos son los intervalos donde Vs aumenta de forma más rápida, no nos va a interesar hacer una
estimación demasiado precisa. Si es que m y n son altos, el comportamiento de la función en esos
intervalos no es de nuestro interés, ya que siempre va a existir una solución mucho más cercana a 0.5,
que es la que nos interesa.

Sumando estas consideraciones, ya tenemos toda la información necesaria para aislar la solución más cercana a 0.5 por la izquierda y por la derecha.

Procedemos a definir la función intervalo, que recibe n,m y retorna el intervalo con la solución más cercana por la izquierda y el intervalo con la solución más cercana por la derecha.

```python
def intervalo(n,m):
    a = 0
    b = 1
    c = 0
    d = 1
    numero = 0
    if n*m == 1:
        return [0,0]

    if n*m == 2: #existe una unica solucion en el intervalo [0,1]
        return [a,b,0,0]

    if n*m ==3: #existe una unica solucion en el intervalo [0,1]
        return [a,b,0,0]

    numero = np.round((n*m)/6)
    if numero == 1:
        b = 0.5

    if numero >= 2:
        b = 0.5
        numero = np.round(0.52*numero)
        if numero == 1:
            a = 0.25
        if numero == 0:
            b = 0.25
        if numero >= 2:
            a = 0.25

        while(numero != 1):
            numero = np.round(0.5*numero)
            if numero == 1:
                a = a+(b-a)/2
                continue
            if numero == 0:
                b = b - (b-a)/2
            if numero >= 2:
                a = a+(b-a)/2

    numero = np.round((n*m)/3)
    if numero == 1:
        c = 0.5

    if numero >= 2:
        c = 0.5
        numero = np.round(0.31*numero)
        if numero == 1:
            d = 0.75
        if numero == 0:
            c = 0.75
            marca = 1
        if numero >= 2:
            d = 0.75
            marca = 0

        while(numero != 1):

            if marca == 0:
                numero = np.round(0.47*numero)
            if marca == 1:
                numero = np.round(0.3*numero)

            if numero == 1:
                d = d - (d-c)/2
                continue
            if numero == 0:
                c = c + (d-c)/2
            if numero >= 2:
                d = d - (d-c)/2
    return [a, b , c, d]

```

Una vez se tienen los intervalos, se puede ocupar Bisección para encontrar la solución.

```python
def f_Biseccion(n,m,tol):

    root2 = 2
    a,b,c,d = intervalo(n,m)
    r = lambda x: construir_polinomio(n,m,x)

    fa = r(a)
    fb = r(b)
    if np.sign(fa*fb) >= 0:
        print('Numeros demasiado grandes!')
        return None

    while(b-a)/2 > tol:
        e = (a+b)/2.
        fe = r(e)
        if fe == 0:
            print(e)
            break
        elif np.sign(fa*fe) < 0:
            b = e
            fb = fe
        else:
            a = e
            fa = fe

    root1 = (a+b)/2.

    if c*d != 0:
        fc = r(c)
        fd = r(d)
        if np.sign(fc*fd) >= 0:
            print('Numeros demasiado grandes!')
            return None

        while(d-c)/2 > tol:
            e = (c+d)/2.
            fe = r(e)
            if fe == 0:
                print(e)
                break
            elif np.sign(fc*fe) < 0:
                d = e
                fd = fe
            else:
                c = e
                fc = fe

        root2 = (a+b)/2.

    if 0.5 - root1 < root2 - 0.5:
        return root1

    return root2

f_Biseccion(9,2,1e-8)
```

    0.4396752044558525

###### Método Iteración de Punto Fijo

Para desarrollar este método, se trabajará con 0.5 como "Initial Guess".
Se contará con dos funciones, que recorrerán hacia la izquierda y hacia la derecha, para encontrar las dos raíces más cercanas a 0.5 por cada lado. Luego, se compararán estas raíces y se elegirá la más cercana a 0.5.

Primero se define una función para graficar:

```python
def cobweb(x, g=None):
    min_x = np.amin(x)
    max_x = np.amax(x)

    pyplot.figure(figsize=(10,10))
    ax = pyplot.axes()
    pyplot.plot(np.array([min_x,max_x]),np.array([min_x,max_x]),'b-')
    for i in np.arange(x.size-1):
        delta_x = x[i+1]-x[i]
        head_length =  np.abs(delta_x)*0.04
        arrow_length = delta_x-np.sign(delta_x)*head_length
        ax.arrow(x[i], x[i], 0, arrow_length, head_width=1.5*head_length, head_length=head_length, fc='k', ec='k')
        ax.arrow(x[i], x[i+1], arrow_length, 0, head_width=1.5*head_length, head_length=head_length, fc='k', ec='k')

    if g!=None:
        y = np.linspace(min_x,max_x,1000)
        pyplot.plot(y,[g(i) for i in y],'r')

    pyplot.title('Cobweb diagram')
    pyplot.grid(True)
    pyplot.show()
```

Tenemos la expresión

\begin{equation}
   f_{n,m}(x) = 0,
\end{equation}

Para poder trabajar con FPI, debemos llegar a una expresión de la forma:

\begin{equation}
    x = g_{n,m}(x),
\end{equation}

Existen muchos caminos para llegar a esta expresión, el más obvio sería:

\begin{equation}
    f_{n,m}(x) = 0 ,
\end{equation}

\begin{equation}
    f_{n,m}(x) + x = x ,
\end{equation}

Veamos como se comporta esta función para n y m cualquiera:

```python
n = 2
m = 2
x = np.arange(-1, 1, 0.0001).tolist()
r = f(n,m)

funcion_2 = lambda x: r(x) + x

pyplot.plot(x, [r(i) for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

pyplot.plot(x, [funcion_2(i) for i in x])
pyplot.plot(x, [i for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()
```

Se puede apreciar que la función cumple con su cometido, tiene como puntos fijos las soluciones de la primera función f(x). Probemos ahora FPI con esta función:

```python
def fpi_prueba(n,m, tol):
    x = []
    x.append(0.5)
    for i in range(3):
        x.append(funcion_2(x[i]))
    cobweb(np.array(x),funcion_2)
    return x[-1]

fpi_prueba(n,m,1e-8)
```

La función diverge.

¿Cuál es el problema?

Si bien la función cumple con los puntos fijos, sus pendientes son muy abruptas. Esto ocasiona que la función sea divergente para la mayoría de los puntos. Para solucionarlo, se propondrá una función mas "suave":

\begin{equation}
   f(x) = 0,
\end{equation}

\begin{equation}
   f(x) + x^3 + ax = x^3 + ax,
\end{equation}

\begin{equation}
   f(x) + x^3 + ax = x(x^2 + a),
\end{equation}

\begin{equation}
   \frac{f(x) + x^3 + ax}{x^2+a} = x,
\end{equation}

La ventaja de esta función es que se puede suavizar aumentando el valor de a.

Probemos FPI con a = 10:

```python
funcion_2 = lambda x: (x**3 + 10*x + r(x)) / (x**2 + 10)

pyplot.plot(x, [funcion_2(i) for i in x])
pyplot.plot(x, [i for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

def fpi_prueba(n,m, tol):
    x = []
    x.append(0.5)

    for i in range(20):
        x.append(funcion_2(x[i]))

    cobweb(np.array(x),funcion_2)
    return x[-1]

fpi_prueba(n,m,1e-8)
```

La función es mucho más suave y se puede apreciar que converge en el punto fijo.

Sin embargo, aún existe un problema. Debido a la concavidad de la función, solo itera hacia la izquierda. Si hubiera una solución más cercana a 0.5 por la derecha no estaríamos realizando lo que se nos pide.

Es por esto que se planteará una segunda función de concavidad opuesta en todo intervalo para que itere de forma contraria:

\begin{equation}
   f(x) = 0,
\end{equation}

\begin{equation}
   f(x) - x^3 - ax = -x^3 - ax,
\end{equation}

\begin{equation}
    x^3 + ax - f(x) = x^3 + ax,
\end{equation}

\begin{equation}
    x^3 + ax - f(x) = x(x^2 + a),
\end{equation}

\begin{equation}
    \frac{x^3 + ax - f(x)}{x^2+a} = x,
\end{equation}

Con a = 10:

```python
funcion_3 = lambda x: (x**3 + 10*x - r(x)) / (x**2 + 10)

pyplot.plot(x, [funcion_3(i) for i in x])
pyplot.plot(x, [i for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

def fpi_prueba(n,m, tol):
    x = []
    x.append(0.5)

    for i in range(20):
        x.append(funcion_3(x[i]))

    cobweb(np.array(x),funcion_3)
    return x[-1]

fpi_prueba(n,m,1e-8)
```

La función converge al punto fijo que está en dirección contraria. Así, podemos definir un FPI que ocupe estas dos funciones para iterar en ambas direcciones.

```python
n = 9
m = 2
r = f(n,m)

if n*m <= 50:
    a = 10

elif n*m <= 80:
    a = 15

elif n*m <= 100:
    a = 20

elif n*m <= 150:
    a = 50

elif n*m <= 300:
    a = 100

funcion_2 = lambda x: (x**3 + a*x - r(x)) / (x**2 + a)
funcion_3 = lambda x: (x**3 + a*x + r(x)) / (x**2 + a)

def f_FPI(n, m, tol):
    lista_left = []
    lista_right= []
    x0 = 0.5
    lista_left.append(x0)
    lista_right.append(x0)
    solucion_left = -2
    solucion_right = -2

    for i in range(1000):
        if i >= 1:
            if solucion_left == -2:
                if np.abs(lista_left[i] - lista_left[i-1]) <= tol:
                    solucion_left = lista_left[-1]

            if solucion_right == -2:
                if np.abs(lista_right[i] - lista_right[i-1]) <= tol:
                    solucion_right = lista_right[-1]

        if solucion_left == -2 and solucion_right == -2:
            if lista_left[-1] >= 1 or lista_left[-1] <= -1:
                if lista_right[-1] >= 1 or lista_right[-1] <= -1:
                    print("diverge")
                    break
                lista_right.append(funcion_2(lista_right[i]))
                continue

            if lista_right[-1] >= 1 or lista_right[-1] <= -1:

                if lista_left[-1] >= 1 or lista_left[-1] <= -1:
                    print("diverge")
                    break
                lista_left.append(funcion_3(lista_left[i]))
                continue

            lista_right.append(funcion_2(lista_right[i]))
            lista_left.append(funcion_3(lista_left[i]))

        if solucion_left == -2 and solucion_right != -2:
            if lista_left[-1] >= 1 or lista_left[-1] <= -1:
                break
            lista_left.append(funcion_3(lista_left[i]))

        if solucion_left != -2 and solucion_right == -2:
            if lista_right[-1] >= 1 or lista_right[-1] <= -1:
                break
            lista_right.append(funcion_2(lista_right[i]))

    if solucion_left == -2 and solucion_right == -2:
        print("diverge")
        return -1
    else:
        cobweb(np.array(lista_right),funcion_2)
        cobweb(np.array(lista_left),funcion_3)
        if abs(0.5 - solucion_left) < abs(0.5 - solucion_right):
            return solucion_left
        else:
            return solucion_right

pyplot.plot(x, [r(i) for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

pyplot.plot(x, [funcion_2(i) for i in x])
pyplot.plot(x, [i for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-1, 1)
pyplot.show()

pyplot.plot(x, [funcion_3(i) for i in x])
pyplot.plot(x, [i for i in x])
pyplot.axhline(0, color="black")
pyplot.axvline(0, color="black")
# Limitar los valores de los ejes.
pyplot.xlim(-1, 1)
pyplot.ylim(-2, 2)
pyplot.show()

f_FPI(n,m,1e-8)
```

# Referencias

cobweb , f_Biseccion : https://github.com/tclaudioe/Scientific-Computing/blob/master/SC1/04_roots_of_1D_equations.ipynb


# Corrugated Shells geometry

## Init symbols for *sympy*

```python
from sympy import *
from geom_util import *
from sympy.vector import CoordSys3D
import matplotlib.pyplot as plt
import sys
sys.path.append("../")

%matplotlib inline

%reload_ext autoreload
%autoreload 2
%aimport geom_util
```

```python
# Any tweaks that normally go in .matplotlibrc, etc., should explicitly go here
%config InlineBackend.figure_format='retina'
plt.rcParams['figure.figsize'] = (12, 12)

plt.rc('text', usetex=True)

plt.rc('font', family='serif')
# SMALL_SIZE = 42
# MEDIUM_SIZE = 42
# BIGGER_SIZE = 42

# plt.rc('font', size=SMALL_SIZE)          # controls default text sizes
# plt.rc('axes', titlesize=SMALL_SIZE)     # fontsize of the axes title
# plt.rc('axes', labelsize=MEDIUM_SIZE)    # fontsize of the x and y labels
# plt.rc('xtick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
# plt.rc('ytick', labelsize=SMALL_SIZE)    # fontsize of the tick labels
# plt.rc('legend', fontsize=SMALL_SIZE)    # legend fontsize
# plt.rc('figure', titlesize=BIGGER_SIZE)  # fontsize of the figure title

init_printing()
```

```python
N = CoordSys3D('N')
alpha1, alpha2, alpha3 = symbols("alpha_1 alpha_2 alpha_3", real = True, positive=True)
```

## Cylindrical coordinates

```python
R, L, ga, gv = symbols("R L g_a g_v", real = True, positive=True)
```

```python
a1 = pi / 2 + (L / 2 - alpha1)/R

a2 = 2 * pi * alpha1 / L

x1 = (R + ga * cos(gv * a2)) * cos(a1)
x2 = alpha2
x3 = (R + ga * cos(gv * a2)) * sin(a1)

r = x1*N.i + x2*N.j + x3*N.k

z = ga/R*gv*sin(gv*a1)
w = 1 + ga/R*cos(gv*a1)

dr1x=(z*cos(a1) + w*sin(a1))
dr1z=(z*sin(a1) - w*cos(a1))

r1 = dr1x*N.i + dr1z*N.k
r2 =N.j

mag=sqrt((w)**2+(z)**2)

nx = -dr1z/mag
nz = dr1x/mag

n = nx*N.i+nz*N.k

dnx=nx.diff(alpha1)
dnz=nz.diff(alpha1)

dn= dnx*N.i+dnz*N.k
```

```python
Ralpha = r+alpha3*n

R1=r1+alpha3*dn
R2=Ralpha.diff(alpha2)
R3=n
```

```python
r1
```

```python
A = r1.dot(r1)
A
```

```python
R1a3x=-1/(mag**3)*(w*cos(a1) - z*sin(a1))*(-1/R*w+ga*gv*gv/(R*R)*cos(gv*a1))*z+(1/mag)*(1/R*w*sin(a1)+ga*gv*gv/(R*R)*cos(gv*a1)*sin(a1)+2/R*z*cos(a1))
R1a3x
```

```python
R1
```

```python
R2
```

```python
R3
```

### Draw

```python
import plot

%aimport plot

x1 = (R + alpha3 + ga * cos(gv * a2)) * cos(a1)
x2 = alpha2
x3 = (R + alpha3 + ga * cos(gv * a2)) * sin(a1)

# x1 = Ralpha.dot(N.i)
# x3 = Ralpha.dot(N.k)

alpha1_x = lambdify([R, L, ga, gv, alpha1, alpha3], x1, "numpy")
alpha3_z = lambdify([R, L, ga, gv, alpha1, alpha3], x3, "numpy")

R_num = 1/0.8
L_num = 2
h_num = 0.1
ga_num = h_num/3
gv_num = 20

x1_start = 0
x1_end = L_num
x3_start = -h_num/2
x3_end = h_num/2

def alpha_to_x(a1, a2, a3):
    x=alpha1_x(R_num, L_num, ga_num, gv_num, a1, a3)
    z=alpha3_z(R_num, L_num, ga_num, gv_num, a1, a3)
    return x, 0, z

plot.plot_init_geometry_2(x1_start, x1_end, x3_start, x3_end, alpha_to_x)
```

```python
%aimport plot

R3_1=R3.dot(N.i)
R3_3=R3.dot(N.k)

R3_1_x = lambdify([R, L, ga, gv, alpha1, alpha3], R3_1, "numpy")
R3_3_z = lambdify([R, L, ga, gv, alpha1, alpha3], R3_3, "numpy")

def R3_to_x(a1, a2, a3):
    x=R3_1_x(R_num, L_num, ga_num, gv_num, a1, a3)
    z=R3_3_z(R_num, L_num, ga_num, gv_num, a1, a3)
    return x, 0, z

plot.plot_vectors(x1_start, x1_end, 0, alpha_to_x, R3_to_x)
```

```python
%aimport plot

R1_1=R1.dot(N.i)
R1_3=R1.dot(N.k)

R1_1_x = lambdify([R, L, ga, gv, alpha1, alpha3], R1_1, "numpy")
R1_3_z = lambdify([R, L, ga, gv, alpha1, alpha3], R1_3, "numpy")

def R1_to_x(a1, a2, a3):
    x=R1_1_x(R_num, L_num, ga_num, gv_num, a1, a3)
    z=R1_3_z(R_num, L_num, ga_num, gv_num, a1, a3)
    return x, 0, z

plot.plot_vectors(x1_start, x1_end, h_num/2, alpha_to_x, R1_to_x)
```

### Lame params

```python
H1 = sqrt((alpha3*((-(1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*sin((L/2 - alpha1)/R) - ga*gv*sin(gv*(pi/2 + (L/2 - alpha1)/R))*cos((L/2 - alpha1)/R)/R)*(-ga*gv*(1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*sin(gv*(pi/2 + (L/2 - alpha1)/R))/R**2 + ga**2*gv**3*sin(gv*(pi/2 + (L/2 - alpha1)/R))*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R**3)/((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)**2 + ga**2*gv**2*sin(gv*(pi/2 + (L/2 - alpha1)/R))**2/R**2)**(3/2) + ((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*cos((L/2 - alpha1)/R)/R + ga*gv**2*cos((L/2 - alpha1)/R)*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R**2 - 2*ga*gv*sin((L/2 - alpha1)/R)*sin(gv*(pi/2 + (L/2 - alpha1)/R))/R**2)/sqrt((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)**2 + ga**2*gv**2*sin(gv*(pi/2 + (L/2 - alpha1)/R))**2/R**2)) + (1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*cos((L/2 - alpha1)/R) - ga*gv*sin((L/2 - alpha1)/R)*sin(gv*(pi/2 + (L/2 - alpha1)/R))/R)**2 + (alpha3*(((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*cos((L/2 - alpha1)/R) - ga*gv*sin((L/2 - alpha1)/R)*sin(gv*(pi/2 + (L/2 - alpha1)/R))/R)*(-ga*gv*(1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*sin(gv*(pi/2 + (L/2 - alpha1)/R))/R**2 + ga**2*gv**3*sin(gv*(pi/2 + (L/2 - alpha1)/R))*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R**3)/((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)**2 + ga**2*gv**2*sin(gv*(pi/2 + (L/2 - alpha1)/R))**2/R**2)**(3/2) + ((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*sin((L/2 - alpha1)/R)/R + ga*gv**2*sin((L/2 - alpha1)/R)*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R**2 + 2*ga*gv*sin(gv*(pi/2 + (L/2 - alpha1)/R))*cos((L/2 - alpha1)/R)/R**2)/sqrt((1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)**2 + ga**2*gv**2*sin(gv*(pi/2 + (L/2 - alpha1)/R))**2/R**2)) + (1 + ga*cos(gv*(pi/2 + (L/2 - alpha1)/R))/R)*sin((L/2 - alpha1)/R) + ga*gv*sin(gv*(pi/2 + (L/2 - alpha1)/R))*cos((L/2 - alpha1)/R)/R)**2)
H2=S(1)
H3=S(1)

H=[H1, H2, H3]
DIM=3
dH = zeros(DIM,DIM)
for i in range(DIM):
    dH[i,0]=H[i].diff(alpha1)
    dH[i,1]=H[i].diff(alpha2)
    dH[i,2]=H[i].diff(alpha3)

trigsimp(H1)

```


# 量子・古典ハイブリッドの量子機械学習アルゴリズムを使って、新しい素粒子現象の発見を目指す

この実習では、**量子・古典ハイブリッドアルゴリズム**の応用である**量子機械学習**の基本的な実装を学んだのち、その活用例として、**素粒子実験での新粒子探索**への応用を考えます。ここで学ぶ量子機械学習の手法は、量子コンピュータを応用することで古典機械学習の性能を向上するという観点から提案された、**変分量子回路**を使った学習手法 [[1]](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.032309)です。その手法の元になる変分法と、それに基づいた変分量子固有値ソルバー法について学んだのち、量子機械学習の学習へと進みます。

## 内容
1. [はじめに](#introduction)
2. [変分法と変分量子回路](#variational_method)
3. [機械学習](#ml)
4. [量子機械学習](#qml)
5. [初歩的な例](#example)
    1. [学習データの準備](#func_data)
    2. [量子状態の生成](#func_state_preparation)
    3. [変分フォームを使った状態変換](#func_ariational_form)
    4. [測定とモデル出力](#func_measurement)
6. [素粒子現象の探索への応用](#susy)
    1. [学習データの準備](#susy_data)
    2. [量子状態の生成](#susy_state_preparation)
    3. [変分フォームを使った状態変換](#susy_ariational_form)
    4. [測定とモデル出力](#susy_measurement)
7. [[課題] VQEの応用](#vqe_application)
8. [参考文献](#references)

## はじめに

近年、機械学習の分野において**深層学習**（**ディープラーニング**）が注目を浴びています。ディープラーニングは**ニューラルネットワーク**の隠れ層を多層にすることで、入力と出力の間の複雑な関係を学習することができます。その学習結果を使って、新しい入力データに対して出力を予測することが可能になります。ここで学習する量子機械学習アルゴリズムは、このニューラルネットワークの部分を変分量子回路に置き換えたものです。つまり、ニューラルネットワークでの各ニューロン層への重みを調節する代わりに、変分量子回路のパラメータ（例えば回転ゲートの回転角）を調整することで入力と出力の関係を学習しようという試みです。
量子力学の重ね合わせの原理から、**指数関数的に増える多数の計算基底**を使って状態を表現できることが量子コンピュータの強みです。この強みを生かすことで、データ間の複雑な相関を学習できる可能性が生まれます。そこに量子機械学習の最も大きな強みがあると考えられています。

多項式で与えられる数の量子ゲートを使って、指数関数的に増える関数を表現できる可能性があるところに量子機械学習の強みがありますが、誤り訂正機能を持たない中規模の量子コンピュータ (*Noisy Intermediate-Scale Quantum*デバイス, 略してNISQ）で、古典計算を上回る性能を発揮できるか確証はありません。しかしNISQデバイスでの動作に適したアルゴリズムであるため、2019年3月にはIBMの実験チームによる実機での実装がすでに行われ、結果も論文[[2]](https://www.nature.com/articles/s41586-019-0980-2)として出版されています。

## 変分法と変分量子回路

変分量子回路の元になるのは、**変分法**（あるいは変分原理）と呼ばれる考え方です。量子力学における変分法と、その方法を用いた変分量子固有値ソルバー法については、この[ノートブック](vqe.ipynb)を参照してください。

## 機械学習と深層学習

機械学習を一言で（大雑把に）説明すると、与えられたデータを元に、ある予測を返すような機械を実現する工程だと言えます。例えば、2種類の変数$\boldsymbol{x}$と$\boldsymbol{y}$からなるデータ（$(x_i, y_i)$を要素とするベクトル、$i$は要素の添字）があったとして、その変数間の関係を求める問題として機械学習を考えてみましょう。つまり、変数$x_i$を引数とする関数$f$を考え、その出力$\tilde{y_i}=f(x_i)$が$\tilde{y}_i\simeq y_i$となるような関数$f$をデータから近似的に求めることに対応します。
一般的に、この関数$f$は変数$x$以外のパラメータを持っているでしょう。なので、そのパラメータ$\boldsymbol{w}$をうまく調整して、$y_i\simeq\tilde{y}_i$となる関数$f=f(x,\boldsymbol{w}^*)$とパラメータ$\boldsymbol{w}^*$を求めることが機械学習の鍵になります。

関数$f$を近似する方法の一つとして、現在主流になっているのが脳のニューロン構造を模式化したニューラルネットワークです。下図に示しているのは、ニューラルネットの基本的な構造です。丸で示しているのが構成ユニット（ニューロン）で、ニューロンを繋ぐ情報の流れを矢印で表しています。ニューラルネットには様々な構造が考えられますが、基本になるのは図に示したような層構造で、前層にあるニューロンの出力が次の層にあるニューロンへの入力になります。入力データ$x$を受ける入力層と出力$\tilde{y}$を出す出力層に加え、中間に複数の"隠れ層"を持つものを総称して深層ニューラルネットワークと呼びます。

では、もう少し数学的なモデルを見てみましょう。$l$層目にある$j$番目のユニット$u_j^l$に対して、前層（$l-1$番目）から$n$個の入力$o_k^{l-1}$ ($k=1,2,\cdots n$) がある場合、入力$o_k^{l-1}$への重みパラメータ$w_k^l$を使って

$$
o_j^l=g\left(\sum_{k=1}^n o_k^{l-1}w_k^l\right)
$$

となる出力$o_j^l$を考えます。図で示すと

になります。関数$g$は活性化関数と呼ばれ、入力に対して非線形な出力を与えます。活性化関数としては、一般的にはシグモイド関数やReLU（Rectified Linear Unit）等の関数が用いられることが多いです。

関数$f(x,\boldsymbol{w}^*)$を求めるために、最適なパラメータ$\boldsymbol{w}^*$を決定するプロセス（学習と呼ばれる）が必要です。そのために、出力$\tilde{y}$とターゲットとなる変数$y$の差を測定する関数$L(\boldsymbol{w})$を考えます（一般に損失関数やコスト関数と呼ばれます）。

$$
L(\boldsymbol{w}) = \frac{1}{N}\sum_{i=1}^N L(f(x_i,\boldsymbol{w}),y_i)
$$

$N$は$(x_i, y_i)$データの数です。この損失関数$L(\boldsymbol{w})$を最小化するパラメータ$\boldsymbol{w}^*$を求めたいわけですが、それには誤差逆伝搬法と呼ばれる手法を使うことができることが知られています。この手法は、$L(\boldsymbol{w})$の各$w$に対する微分係数$\Delta_w L(\boldsymbol{w})$を求めて、

$$
w'=w-\epsilon\Delta_w L(\boldsymbol{w})
$$

のように$w$を更新することで、$L(\boldsymbol{w})$を最小化するというものです（$w$と$w'$は更新前と更新後のパラメータ）。$\epsilon\:(>0)$は学習率と呼ばれるパラメータで、これは基本的には私たちが手で決めてやる必要があります。

## 量子機械学習

変分量子回路を用いた量子機械学習アルゴリズムは、一般的には以下のような順番で量子回路に実装され、計算が行われます。

1. **学習データ**$\{(\boldsymbol{x}_i, y_i)\}$を用意する。$\boldsymbol{x}_i$は入力データのベクトル、$y_i$は入力データに対する真の値（教師データ）とする（$i$は学習データのサンプルを表す添字）。
2. 入力$\boldsymbol{x}$から何らかの規則で決まる回路$U_{\text{in}}(\boldsymbol{x})$（**特徴量マップ**と呼ぶ）を用意し、$\boldsymbol{x}_i$の情報を埋め込んだ入力状態$|\psi_{\rm in}(\boldsymbol{x}_i)\rangle = U_{\text{in}}(\boldsymbol{x}_i)|0\rangle$を作る。
3. 入力状態にパラメータ$\boldsymbol{\theta}$に依存したゲート$U(\boldsymbol{\theta})$（**変分フォーム**）を掛けたものを出力状態$|\psi_{\rm out}(\boldsymbol{x}_i,\boldsymbol{\theta})\rangle = U(\boldsymbol{\theta})|\psi_{\rm in}(\boldsymbol{x}_i)\rangle$とする。
4. 出力状態のもとで何らかの**観測量**を測定し、測定値$O$を得る。例えば、最初の量子ビットで測定したパウリ$Z$演算子の期待値$\langle Z_1\rangle = \langle \psi_{\rm out} |Z_1|\psi_{\rm out} \rangle$などを考える。
5. $F$を適当な関数として、$F(O)$をモデルの出力$y(\boldsymbol{x}_i,\boldsymbol{\theta})$とする。
6. 真の値$y_i$と出力$y(\boldsymbol{x}_i,\boldsymbol{\theta})$の間の乖離を表す**コスト関数**$L(\boldsymbol{\theta})$を定義し、古典計算でコスト関数を計算する。
7. $L(\boldsymbol{\theta})$が小さくなるように$\boldsymbol{\theta}$を更新する。
7. 3-7のプロセスを繰り返すことで、コスト関数を最小化する$\boldsymbol{\theta}=\boldsymbol{\theta^*}$を求める。
8. $y(\boldsymbol{x},\boldsymbol{\theta^*})$が学習によって得られた**予測モデル**になる。

この順に量子機械学習アルゴリズムを実装していきましょう。まず、必要なライブラリを最初にインポートします。

```python
# Tested with python 3.7.9, qiskit 0.23.5, numpy 1.20.1

import numpy as np
import matplotlib.pyplot as plt

from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import Aer, execute
from qiskit.aqua.components.optimizers import SPSA, COBYLA

import logging
from qiskit.aqua import set_qiskit_aqua_logging
set_qiskit_aqua_logging(logging.DEBUG)  # choose INFO, DEBUG to see the log
```

## 初歩的な例

ある入力$\{x_i\}$と、既知の関数$f$による出力$y_i=f(x_i)$が学習データとして与えられた時に、そのデータから関数$f$を近似的に求める問題を考えてみます。例として、$f(x)=x^3$としてみます。

### 学習データの準備

まず、学習データを準備します。$x_{\text{min}}$と$x_{\text{max}}$の範囲でデータをnum_x_train個ランダムに取った後、正規分布に従うノイズを追加しておきます。nqubitが量子ビット数、c_depthが変分フォーム回路の深さ（後述）を表します。

```python
random_seed = 0
np.random.seed(random_seed)

# Qubit数、回路の深さ、訓練サンプル数の定義など
nqubit = 3
c_depth = 5
x_min = -1.; x_max = 1.; num_x_train = 30

# 関数の定義
func_to_learn = lambda x: x**3
x_train = x_min + (x_max - x_min) * np.random.rand(num_x_train)
y_train = func_to_learn(x_train)

# 関数に正規分布ノイズを付加
mag_noise = 0.05
y_train_noise = y_train + mag_noise * np.random.randn(num_x_train)
```

### 量子状態の生成

次に、入力$x_i$を初期状態$|0\rangle^{\otimes n}$に埋め込むための回路$U_{\rm in}(x_i)$（特徴量マップ）を作成します。まず参考文献 [[1]](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.032309)に従い、回転ゲート$R_j^Y(\theta)=e^{i\theta Y_j/2}$と$R_j^Z(\theta)=e^{i\theta Z_j/2}$を使って

$$
U_{\rm in}(x_i) = \prod_j R_j^Z(\cos^{-1}(x^2))R_j^Y(\sin^{-1}(x))
$$

と定義します。この$U_{\rm in}(x_i)$をゼロの標準状態に適用することで、入力$x_i$は$|\psi_{\rm in}(x_i)\rangle=U_{\rm in}(x_i)|0\rangle^{\otimes n}$という量子状態に変換されることになります。

```python
def U_in(x, nqubit):
    qr = QuantumRegister(nqubit)
    U = QuantumCircuit(qr)

    angle_y = np.arcsin(x)
    angle_z = np.arccos(x**2)

    for i in range(nqubit):
        U.ry(angle_y, i)
        U.rz(angle_z, i)

    return U
```

### 変分フォームを使った状態変換

#### 変分量子回路$U(\boldsymbol{\theta})$の構成
次に、最適化すべき変分量子回路$U(\boldsymbol{\theta})$を作っていきます。これは以下の3つの手順で行います。

1. 2量子ビットゲートの作成（$\to$ 量子ビットをエンタングルさせる）
2. 回転ゲートの作成
3. 1.と2.のゲートを交互に組み合わせ、1つの大きな変分量子回路$U(\boldsymbol{\theta})$を作る

#### 2量子ビットゲートの作成
ここではControlled-$Z$ゲート（$CZ$）を使ってエンタングルさせ、モデルの表現能力を上げることを目指します。

#### 回転ゲートと$U(\boldsymbol{\theta})$の作成
$CZ$ゲートを使ってエンタングルメントを生成する回路$U_{\text{ent}}$と、$j \:(=1,2,\cdots n)$番目の量子ビットに適用する回転ゲート

\begin{align}
U_{\text{rot}}(\theta_j^l) = R_j^Y(\theta_{j3}^l)R_j^Z(\theta_{j2}^l)R_j^Y(\theta_{j1}^l)
\end{align}

を掛けたものを組み合わせて、変分量子回路$U(\boldsymbol{\theta})$を構成します。ここで$l$は量子回路の層を表していて、$U_{\text{ent}}$と上記の回転ゲートを合計$d$層繰り返すことを意味しています。実際は、この演習では最初に回転ゲート$U_{\text{rot}}$を一度適用してから$d$層繰り返す構造を使うため、全体としては

\begin{align}
U\left(\{\theta_j^l\}\right) = \prod_{l=1}^d\left(\left(\prod_{j=1}^n U_{\text{rot}}(\theta_j^l)\right) \cdot U_{\text{ent}}\right)\cdot\prod_{j=1}^n U_{\text{rot}}(\theta_j^0)
\end{align}

という形式の変分量子回路を用いることになります。つまり、変分量子回路は全体で$3n(d+1)$個のパラメータを含んでいます。$\boldsymbol{\theta}$の初期値ですが、$[0, 2\pi]$の範囲でランダムに設定するものとします。

```python
def U_out(nqubit, params):
    qr = QuantumRegister(nqubit)
    #cr = ClassicalRegister(nqubit)
    U = QuantumCircuit(qr)

    for i in range(nqubit):
        U.ry(params[i], i)
        U.rz(params[nqubit+i], i)
        U.ry(params[nqubit*2+i], i)
    for d in range(c_depth):
        for j in range(nqubit-1):
            U.cz(j, j+1)
        U.cz(nqubit-1, 0)
        for i in range(nqubit):
            U.ry(params[nqubit*(3*d+3)+i], i)
            U.rz(params[nqubit*(3*d+4)+i], i)
            U.ry(params[nqubit*(3*d+5)+i], i)

    return U
```

### 測定とモデル出力

モデルの出力（予測値）として、状態$|\psi_{\rm out}(\boldsymbol{x},\boldsymbol{\theta})\rangle=U(\boldsymbol{\theta})|\psi_{\rm in}(\boldsymbol{x})\rangle$の元で最初の量子ビットを$Z$基底で測定した時の期待値を使うことにします。つまり$y(\boldsymbol{x},\boldsymbol{\theta}) = \langle Z_0(\boldsymbol{x},\boldsymbol{\theta}) \rangle = \langle \psi_{\rm out}(\boldsymbol{x},\boldsymbol{\theta})|Z_0|\psi_{\rm out}(\boldsymbol{x},\boldsymbol{\theta})\rangle$です。

```python
def pred_circ(x, nqubit, params):

    qr = QuantumRegister(nqubit, name='q')
    cr = ClassicalRegister(1, name='c')
    circ = QuantumCircuit(qr, cr)

    u_in = U_in(x, nqubit).to_instruction()
    u_out = U_out(nqubit, params).to_instruction()

    circ.append(u_in, qr)
    circ.append(u_out, qr)
    circ.measure(0, 0)

    return circ

backend = Aer.get_backend("qasm_simulator")
NUM_SHOTS = 10000

def objective_function(params):
    cost_total = 0
    for i in range(len(x_train)):
        qc = pred_circ(x_train[i], nqubit, params)
        result = execute(qc, backend, shots=NUM_SHOTS).result()
        counts = result.get_counts(qc)
        exp_2Z = (2*counts['0']-2*counts['1'])/NUM_SHOTS
        cost = (y_train_noise[i] - exp_2Z)**2
        cost_total += cost

    return cost_total
```

ここで0と1の測定結果（固有値+1と-1）に2を掛けているのは、$Z$基底での測定結果の範囲を広げるためです。コスト関数$L$として、モデルの予測値と真の値$y_i$の平均2乗誤差の総和を使っています。

では、最後にこの回路を実行して、結果を見てみましょう。

```python
num_vars = nqubit*3*(c_depth+1)
params = np.random.rand(num_vars)*2*np.pi

optimizer = COBYLA(maxiter=500, tol=0.0001)
ret = optimizer.optimize(num_vars=num_vars, objective_function=objective_function, initial_point=params)
print('ret[0] =',ret[0])

x_list = np.arange(x_min, x_max, 0.02)
y_pred = []
for x in x_list:
    qc = pred_circ(x, nqubit, ret[0])
    result = execute(qc, backend, shots=NUM_SHOTS).result()
    counts = result.get_counts(qc)
    exp_2Z = (2*counts['0']-2*counts['1'])/NUM_SHOTS
    y_pred.append(exp_2Z)

plt.plot(x_train, y_train_noise, "o", label='Training Data (w/ Noise)')
plt.plot(x_list, func_to_learn(x_list), label='Original Function')
plt.plot(x_list, np.array(y_pred), label='Predicted Function')
plt.legend()
plt.show()
```

生成された図を確認してください。ノイズを印加した学習データの分布から、元の関数$f(x)=x^3$をほぼ導き出せていることが分かると思います。

## 素粒子現象の探索への応用

次の実習課題では、（**標準模型**と呼ばれる）素粒子現象の基本理論を超える新しい物理現象として、**超対称性理論**（*Supersymmetry*、略してSUSY）から予言される新粒子の探索を考えてみます。下の図はSUSY信号（左側）と標準模型でのバックグラウンド（右側）のファインマンダイアグラムを表していて、この二つの物理過程を量子機械学習で分類することを試みます。

(図の引用：参考文献[[3]](https://www.nature.com/articles/ncomms5308))

### 学習データの準備

学習に用いるデータは、カリフォルニア大学アーバイン校（UC Irvine）の研究グループが提供する[機械学習レポジトリ](https://archive.ics.uci.edu/ml/index.php)の中の[SUSYデータセット](https://archive.ics.uci.edu/ml/datasets/SUSY)です。このデータセットの詳細は文献[[3]](https://www.nature.com/articles/ncomms5308)に委ねますが、ある特定のSUSY粒子生成反応とそれに良く似た特徴を持つ背景事象を検出器で観測した時に予想される信号（運動学的変数）をシミュレートしたデータが含まれています。

以下では、まず学習に使う運動学的変数を選び、その変数を指定したサンプルを訓練用とテスト用に準備します。

```python
import pandas as pd
from qiskit.aqua import QuantumInstance
from qiskit.circuit.library import TwoLocal, ZFeatureMap, ZZFeatureMap
from qiskit.aqua.algorithms import VQC
from qiskit.aqua.utils import split_dataset_to_data_and_labels, map_label_to_class_name

df = pd.read_csv("data_files/SUSY_1K.csv",
                 names=('isSignal','lep1_pt','lep1_eta','lep1_phi','lep2_pt','lep2_eta',
                        'lep2_phi','miss_ene','miss_phi','MET_rel','axial_MET','M_R','M_TR_2',
                        'R','MT2','S_R','M_Delta_R','dPhi_r_b','cos_theta_r1'))

feature_dim = 3  # dimension of each data point
if feature_dim == 3:
    SelectedFeatures = ['lep1_pt', 'lep2_pt', 'miss_ene']
elif feature_dim == 5:
    SelectedFeatures = ['lep1_pt','lep2_pt','miss_ene','M_TR_2','M_Delta_R']
elif feature_dim == 7:
    SelectedFeatures = ['lep1_pt','lep1_eta','lep2_pt','lep2_eta','miss_ene','M_TR_2','M_Delta_R']

training_size = 20
testing_size = 20
niter = 500
random_seed = 10598

df_sig = df.loc[df.isSignal==1, SelectedFeatures]
df_bkg = df.loc[df.isSignal==0, SelectedFeatures]

df_sig_training = df_sig.values[:training_size]
df_bkg_training = df_bkg.values[:training_size]
df_sig_test = df_sig.values[training_size:training_size+testing_size]
df_bkg_test = df_bkg.values[training_size:training_size+testing_size]
training_input = {'1':df_sig_training, '0':df_bkg_training}
test_input = {'1':df_sig_test, '0':df_bkg_test}
#print('train_input =',training_input)
#print('test_input =',test_input)

datapoints, class_to_label = split_dataset_to_data_and_labels(test_input)
datapoints_tr, class_to_label_tr = split_dataset_to_data_and_labels(training_input)
```

### 量子状態の生成

次は特徴量マップ$U_{\rm in}(\boldsymbol{x}_i)$の作成ですが、ここでは参考文献[[2]](https://www.nature.com/articles/s41586-019-0980-2)に従い、

\begin{align}
U_{\phi_{\{k\}}}(\boldsymbol{x}_i)=\exp\left(i\phi_{\{k\}}(\boldsymbol{x}_i)Z_k\right)
\end{align}

とします（$k$は入力値$\boldsymbol{x}_i$のベクトル要素の添字）。ここで$\phi_{\{k\}}(\boldsymbol{x}_i)=x_i^k$と決めて（$x_i^k$は$\boldsymbol{x}_i$の$k$番目要素）、入力値$\boldsymbol{x}_i$を$k$個の量子ビットに埋め込みます。この$U_{\phi_{\{k\}}}(x)$にアダマール演算子を組み合わせることで、全体として

\begin{align}
U_{\rm in}(\boldsymbol{x}_i) = U_{\phi}(\boldsymbol{x}_i) H^{\otimes n},\:\:U_{\phi}(\boldsymbol{x}_i) = \exp\left(i \sum_{k=1}^n \phi_{\{k\}}(\boldsymbol{x}_i)Z_k\right)
\end{align}

が得られます。

```python
feature_map = ZFeatureMap(feature_dim, reps=1)
```

### 変分フォームを使った状態変換

変分量子回路$U(\boldsymbol{\theta})$は上の初歩的な例で用いた回路とほぼ同じですが、回転ゲートとして

\begin{align}
U_{\text{rot}}(\theta_j^l) = R_j^Y(\theta_{j1}^l)R_j^Z(\theta_{j2}^l)
\end{align}

を使います。上の例では$U(\boldsymbol{\theta})$を自分で組み立てましたが、Qiskitにはこの$U(\boldsymbol{\theta})$を実装するAPIがすでに準備されているので、ここではそれを使います。

```python
two = TwoLocal(feature_dim, ['ry','rz'], 'cz', 'full', reps=1)
print(two)
```

### 測定とモデル出力

測定やパラメータの最適化、コスト関数の定義も初歩的な例で用いたものとほぼ同じです。QiskitのAPIを用いるので、プログラムはかなり簡略化されています。

```python
# シミュレータで実行する場合
backend = Aer.get_backend('qasm_simulator')

# 量子コンピュータで実行する場合
#from qiskit import IBMQ
#IBMQ.load_account()
#provider0 = IBMQ.get_provider(hub='ibm-q', group='open', project='main')
#backend_name = 'ibmq_santiago'
#backend = provider0.get_backend(backend_name)

optimizer = COBYLA(maxiter=niter, disp=True)
vqc = VQC(optimizer, feature_map, two, training_input, test_input)

quantum_instance = QuantumInstance(backend=backend, shots=1024,
                                   seed_simulator=random_seed, seed_transpiler=random_seed,
                                   skip_qobj_validation=True)

result = vqc.run(quantum_instance)
print(" --- Testing success ratio: ", result['testing_accuracy'])
```

学習したモデルの出力に対して、閾値を設定することで信号とバックグラウンドの選別が可能になります。その選別性能を評価するために一般的に行われるのは、閾値を連続的に変化させた時に得られる選別効率を2次元平面上でプロットすることです。この曲線をROC（Receiver Operating Characteristic）曲線と言います。

学習結果を使って、訓練用とテスト用のデータからROC曲線を描いてみます。

```python
predicted_probs, predicted_labels = vqc.predict(datapoints[0])
prob_test_signal = predicted_probs[:,1]
#predicted_classes = map_label_to_class_name(predicted_labels, vqc.label_to_class)
#print(" ---   Prediction:   {}".format(predicted_classes))
predicted_probs_tr, predicted_labels_tr = vqc.predict(datapoints_tr[0])
prob_train_signal = predicted_probs_tr[:,1]

from sklearn.metrics import roc_curve, auc, roc_auc_score
fpr, tpr, thresholds = roc_curve(datapoints[1], prob_test_signal, drop_intermediate=False)
fpr_tr, tpr_tr, thresholds_tr = roc_curve(datapoints_tr[1], prob_train_signal, drop_intermediate=False)
roc_auc = auc(fpr, tpr)
roc_auc_tr = auc(fpr_tr, tpr_tr)

plt.plot(fpr, tpr, color='darkorange', lw=2, label='Testing Data (AUC = %0.3f)' % roc_auc)
plt.plot(fpr_tr, tpr_tr, color='darkblue', lw=2, label='Training Data (AUC = %0.3f)' % roc_auc_tr)
plt.plot([0, 0], [1, 1], color='navy', lw=2, linestyle='--')
plt.xlim([-0.05, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend(loc="lower right")
plt.show()
```

## [課題] 素粒子実験へのVQEの応用

ここでは、高エネルギー物理実験にVQEを応用できるかどうかを考えます。その応用例と課題をこの[ノートブック](vqe_tracking.ipynb)に準備しました。最後にある課題をレポートとして提出してください。

## 参考文献
1. K. Mitarai, M. Negoro, M. Kitagawa, and K. Fujii, “Quantum circuit learning”, [Phys. Rev. A 98, 032309 (2018)](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.032309)
2. V. Havlicek _et al._ , “Supervised learning with quantum-enhanced feature spaces”, [Nature 567, 209–212 (2019)](https://www.nature.com/articles/s41586-019-0980-2)
3. P. Baldi, P. Sadowski, and D. Whiteson, “Searching for exotic particles in high-energy physics with deep learning”, [Nature Commun. 5, 4308 (2014)](https://www.nature.com/articles/ncomms5308)


# ATSC 507 Assignment V - Due Feb 24

```
__author__ = 'Yingkai (Kyle) Sha'
__email__  = 'yingkai@eos.ubc.ca'
```

```
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
import matplotlib.lines as mline
import matplotlib.patches as mpat
from mpl_toolkits.mplot3d.axes3d import Axes3D
% matplotlib inline
```

```
from matplotlib import rcParams
rcParams.update({'font.size': 12}) # , 'font.family': 'serif'
```

# Content

1. [**Task I: Numerical intergal problem**](#Task-I:-Numerical-intergal-problem)
  2. [Function and plot](#Function-and-plot)
  2. [Analytical integral](#Analytical-integral)
  2. [Gauss quadrature](#Gauss-quadrature)
1. [**Task II: GFS & ECMWF resolutions**](#Task-II:-GFS-&-ECMWF-resolutions)
  2. [NCEP-GFS](#NCEP-GFS)
  2. [ECMWF](#ECMWF)
  2. [Spectral truncation to resolution](#Spectral-truncation-to-resolution)
1. [**Task III: Coiffier Eq. 4.22**](#Task-III:-Coiffier-Eq.-4.22)
  2. [Eq. 4.2.2](#Eq-4.22)
  2. [Calculation & Plots](#Calculation-&-Plots)
1. [**Task IV: Spectral methods solving PDE**](#Task-IV:-Spectral-methods-solving-PDE)
  2. [Numerical scheme](#Numerical-scheme)
  2. [Solution & discussions](#Solution-&-discussions)

# Task I: Numerical intergal problem

## Function and plot

Our polynomial:

\begin{equation*}
    y = \frac{x}{2} + x \cdot sin(\frac{\pi}{5}x), \qquad x\ \epsilon\ [0,\ 20]
\end{equation*}

```
def atsc_fun(x):
    return 0.5*x + x*np.sin(2*np.pi*x/10.)
```

Plot our function.

```
x = np.linspace(-5, 25, 100)
x_section = np.linspace(0, 20, 100)
# Figure
fig=plt.figure(figsize=(10, 6))
ax=fig.gca()
patch_handle = mpat.Patch(color=[0, 0.2, 0.4], label='interval $x\ \epsilon\ \mathrm{[0,\ 20]}$')
ax.fill_between(x_section, atsc_fun(x_section), color=[0, 0.2, 0.4], alpha=0.5)
line_handle = mline.Line2D(x, atsc_fun(x), color='k', linewidth=3, label='polynomial curve')
ax.plot(x, atsc_fun(x), 'k-', linewidth=3)
ax.legend(handles=[patch_handle, line_handle], shadow=True, fancybox=True, loc=2, fontsize=12)
ax.grid(); ax.set_xlabel('x', fontsize=14); ax.set_ylabel('y', fontsize=14)
ax.set_title('Polynomial', fontsize=14, fontweight='bold')
```

## Analytical integral

Symbolic integral & Definite integral

```
x = sp.symbols('x')
fun = sp.integrate(0.5*x + x*sp.sin(2*sp.pi*x/10), x)
val = sp.integrate(0.5*x + x*sp.sin(2*sp.pi*x/10), (x, 0, 20))
print('Symbolic integral:\n\t{}'.format(fun))
print('Definite integral:\n\t{} (0 < x < 20)'.format(val))
```

    Symbolic integral:
    	0.25*x**2 - 5*x*cos(pi*x/5)/pi + 25*sin(pi*x/5)/pi**2
    Definite integral:
    	-100/pi + 100.0 (0 < x < 20)

"-100/pi + 100.0" is actually:

```
ans=-100/np.pi + 100.0
print(ans)
```

    68.1690113816

## Gauss quadrature

From **Wikipedia - Gaussian quadrature**:
> n-point Gaussian quadrature is a numerical quadrature rule constructed to yield an exact result for polynomials of degree $\mathrm{2n-1}$ or less by a suitable choice of the points $x_i$ and weights $\omega_i$ for $n = 1, 2, \ldots, n$.

> \begin{equation*}
    \int_{-1}^1 f(x)\,dx \approx \sum_{i=1}^n w_i f(x_i)
  \end{equation*}

> Gaussian quadrature will only produce accurate results if the function $f(x)$ is well approximated by a polynomial function within the range $\mathrm{[−1,\ 1]}$. For the simplest integration problem, the associated polynomials are **Legendre polynomials** $P_n(x)$, and the method is usually known as Gauss–Legendre quadrature. With the n-th polynomial normalized to give $P_n(1)=1$, the i-th Gauss node, $x_i$, is the i-th root of $P_n$; its weight is:

> \begin{equation*}
    w_i = \frac{2}{\left( 1-x_i^2 \right) [P'_n(x_i)]^2}
  \end{equation*}

Here we work in the range of $x\ \epsilon\ \mathrm{[0,\ 20]}$, so we will **change the interval** in the following way:

> \begin{equation*}
    \int_a^b f(x)\,dx \approx \frac{b-a}{2} \sum_{i=1}^n w_i f\left(\frac{b-a}{2}z_i + \frac{a+b}{2}\right)
  \end{equation*}

By looking at our function, Gauss-Legendre method works.

Here I use `numpy.polynomial.legendre.leggauss` calculates the root of Legendre polynomials $x_i$ and weights $\omega_i$. (I can also use Table A-1 in the handout, but I don't want to type).

```
def Gauss_Legendre(fun_name, a, b, n):
    '''
    Gauss-Legendre integral
    ============================================
    Input:
        fun_name: name of user defined function
        a, b: range of definite integral
        n: order of Legendre polynomials
    ============================================
    '''
    # calculate roots and weights
    x, w = np.polynomial.legendre.leggauss(n)
    dx = (b+a)/2*np.ones(x.shape)+((b-a)/2)*x
    # validate user defined function
    df=locals()['fun_name'](dx)
    result=((b-a)/2)*np.sum(w*df)
    return dx, df, result
```

### Results

Testing `Gauss_Legendre` with differnt Legendre polynomials' roots

```
results=np.empty([10])
print('   Gauss-Legendre quadrature\n------------------------------\n    roots (n)\tresults')
for i in range(1, 11):
    _, _, results[i-1] = Gauss_Legendre(atsc_fun, 0, 20, i)
    print('\t{}\t {}'.format(i, results[i-1]))

```

       Gauss-Legendre quadrature
    ------------------------------
        roots (n)	results
    	1	 100.0
    	2	 46.0641488628
    	3	 14.9594755249
    	4	 91.5555074473
    	5	 63.7902019494
    	6	 68.6456169753
    	7	 68.1345693132
    	8	 68.1707961084
    	9	 68.168941584
    	10	 68.1690135191

### Discussions

```
fig=plt.figure(figsize=(10, 6))
ax=fig.gca()
ax.plot(range(1, 11), results, 'o', color='k', mec='k', mfc='gray', markersize=12, label='Gauss-Legendre quadrature')
ax.plot(range(0, 12), ans*np.ones([12]), 'k-', linewidth=3, label='analytical solution')
ax.legend(loc=1, fontsize=12)
ax.set_xlim([0, 11]); ax.set_ylim([10, 110]); ax.grid()
ax.set_xlabel('roots (n)', fontsize=12);
ax.set_ylabel('results', fontsize=12)
ax.set_title('How Gauss-Legendre quadrature approaches to analytical solution', fontsize=14, fontweight='bold')
```

Except the case n=1, all cases when n is an odd number, numerical integal under estimates the solution, and even number cases over estimate the solution.

# Task II: GFS & ECMWF resolutions

## NCEP-GFS

According to **MetEd - GFS Introduction**:

On 18 March 1981, when GFS first bulit, it has
>
* **T24L12** from 48 to 84 hours.
* **T24L6** from 84 to 144 hours (TPBs 282A and 282B).

On May 31, 2005
>
* **T574L64** for 192 hours' run.
* **T190L64** for 384 hours' run.

Based on **GFS online documentation**, latest version has:
>
* Semi-Lagrangian **T1534L64** for 240 hours' run
* semi-Lagrangian **T574L64** for 384 hours'run

## ECMWF

By refering to **ECMWF - What's horizontal resolution**, EC's products have the following spectral truncation:

>

## Spectral truncation to resolution

According to Warner's L1-L4 on p49 (also in Laprise, 1992), resolution of spectral model can be defined as:
$$
    L_1 = \frac{2\pi a}{3K+1} \qquad L_2 = \frac{\pi a}{K} \qquad L_3 = \frac{2\sqrt{\pi}a}{K+1} \qquad L_4 = \frac{\sqrt{2}\pi a}{K}
$$
So here I calculated the corresponds resolution for some of the truncations above:

```
def trunc_res(K):
    K=np.array(K)
    a =6378.1 #km
    return 2.*np.pi*a/(3.*K+1.), np.pi*a/K, 2.*np.sqrt(np.pi)*a/(K+1.), np.sqrt(2.*np.pi*a)/K
```

```
GFS = [24, 190, 574, 1534]; ECMWF = [159, 255, 319, 639, 1279]
GFS_res = trunc_res(GFS); EC_res = trunc_res(ECMWF)
print('GFS spectral resolution\n====================================================================================')
for i in range(4):
    print('T'+str(GFS[i])+' ---> L1: {}, L2: {}, L3: {}, L4: {}'.format(GFS_res[0][i], GFS_res[1][i], GFS_res[2][i], GFS_res[3][i]))
print('\nECMWF spectral resolution\n====================================================================================')
for i in range(5):
    print('T'+str(ECMWF[i])+' ---> L1: {}, L2: {}, L3: {}, L4: {}'.format(EC_res[0][i], EC_res[1][i], EC_res[2][i], EC_res[3][i]))
```

    GFS spectral resolution
    ====================================================================================
    T24 ---> L1: 548.969646681, L2: 834.891337661, L3: 904.391032517, L4: 8.3411197173
    T190 ---> L1: 70.183509996, L2: 105.459958441, L3: 118.375789596, L4: 1.05361512219
    T574 ---> L1: 23.2587255994, L2: 34.908348613, L3: 39.3213492399, L4: 0.348757618842
    T1534 ---> L1: 8.70623163322, L2: 13.0621852046, L3: 14.7294956436, L4: 0.13049991735

    ECMWF spectral resolution
    ====================================================================================
    T159 ---> L1: 83.8384606856, L2: 126.021333987, L3: 141.311098831, L4: 1.25903693846
    T255 ---> L1: 52.3169506628, L2: 78.5780082504, L3: 88.3194367692, L4: 0.785046561629
    T319 ---> L1: 41.831716292, L2: 62.8131413914, L3: 70.6555494154, L4: 0.627545057101
    T639 ---> L1: 20.8940480749, L2: 31.3574211328, L3: 35.3277747077, L4: 0.31328149173
    T1279 ---> L1: 10.4415800437, L2: 15.6664519968, L3: 17.6638873538, L4: 0.156518274601

# Task III: Coiffier Eq. 4.22

Coiffier eq.(4.22) defines fully normalized associated Legendre function:

> \begin{equation*}
    P_n^m\left( \mu \right) = \sqrt{\left( 2n+1 \right)\frac{\left( n-m \right)!}{\left( n+m \right)!}} \frac{\left( 1-\mu^2 \right)^{\frac{m}{2}}}{2^nn!}\frac{d^{n+m}}{d \mu^{n+m}}\left( \mu^2-1 \right)^n
  \end{equation*}

And we know that, part of eq.(4.22) is the conventional associated Legendre functions:

\begin{equation*}
    P_n^m\left( \mu \right) = \frac{\left( 1-\mu^2 \right)^{\frac{m}{2}}}{2^nn!}\frac{d^{n+m}}{d \mu^{n+m}}\left( \mu^2-1 \right)^n
  \end{equation*}

But we cannot use equtions above due to the instability generated by high-order derivative term.

According to `Abramowitz and Stegun (1972)`, associated Legendre functions has recursive form as:

  \begin{equation*}
    P_{n+1}^0\left( \mu \right) = (2n+1)xP_n^0\left( \mu \right) - nP_{n-1}^0\left( \mu \right) \\
    P_{n}^{n}\left( \mu \right) = (2n-1)P_1^1\left( \mu \right)P_{n-1}^{n-1}\left( \mu \right) \\
    P_n^m\left( \mu \right) = P_{n-2}^m\left( \mu \right) + (2n-1)P_1^1\left( \mu \right)P_{n-1}^{m-1}\left( \mu \right)
  \end{equation*}

With initial conditions for $P_0^0\left( \mu \right)$, $P_1^0\left( \mu \right)$, etc.

This is the algorithm I applied, more details please see:

* `Abramowitz, M. and I. A. Stegun, 1972. Handbook of Mathematical Functions. Dover Publications, Inc., New York, pp. 1046.`
* **MITgcm Guidebook, 3.7 Fully normalized associated Legendre functions**

## Eq. 4.22

Here I defined `assoc_legendre` for associated and `norm_legendre` for normalized associated Legendre function group.

```
# factorial
from scipy.misc import factorial
# odd factorial
def odd_factorial(N):
    out=N
    while N >= 3:
        N -= 2
        out *= N
    return out
# Associated Legendre function
def assoc_legendre(m, n, x):
    '''
    Associated Legendre function
    =================================
    Input:
        n: order of Legendre function
        m: degree of association
        x: series -1< x < 1
    =================================
    '''
    if m < 0 or m > n:
        raise ValueError('Input error, m must be positive and smaller than n')
    # Initial cases.
    if (m == 0):
        p_n=1.0
    else:
        s=1
        if (m & 1):
            s=-1
        z=np.sqrt(1-x**2)
        p_n=s*odd_factorial(2*m - 1)*z**m
    if (m == n):
        return p_n
    # recursion.
    p_n_prev=p_n
    p_n=x*(2*m+1)*p_n
    if (n == (m+1)):
        return p_n
    # Iterate the recursion
    for i in range(m+2, n+1):
        result=(x*(2*i-1)*p_n-(i+m-1)*p_n_prev)/(i-m)
        p_n_prev=p_n
        p_n=result
    return result
# Normalized Legendre function
def norm_legendre(m, n, x):
    '''
    Normalized associated Legendre function
        supports NumPy arrary
    '''
    L=len(x)
    result=np.empty(L)
    for i in range(L):
        result[i]=assoc_legendre(m, n, x[i])
    result=result*np.sqrt((2*n+1)*factorial(n-m)/factorial(n+m))
    return result
```

## Calculation & Plots

Homework requires:

    (a) m=0 with n=1 to 4
    (b) m=1 with n=1 to 5,
    (c) m=2 with n=2 to 6.

```
m=0; n=[1, 2, 3, 4]
x=np.linspace(-1, 1, 100)
P0n = np.empty([4, 100])
for i in range(4):
    P0n[i, :]=norm_legendre(m, n[i], x)

m=1; n=[1, 2, 3, 4, 5]
x=np.linspace(-1, 1, 100)
P1n = np.empty([5, 100])
for i in range(5):
    P1n[i, :]=norm_legendre(m, n[i], x)

m=2; n=[2, 3, 4, 5, 6]
x=np.linspace(-1, 1, 100)
P2n = np.empty([5, 100])
for i in range(5):
    P2n[i, :]=norm_legendre(m, n[i], x)
```

Degree (m) = 0.

```
fig=plt.figure(figsize=(10, 6))
ax=fig.gca()
ax.plot(x, P0n[0, :], linewidth=3, label='n = 1')
ax.plot(x, P0n[1, :], linewidth=3, label='n = 2')
ax.plot(x, P0n[2, :], linewidth=3, label='n = 3')
ax.plot(x, P0n[3, :], linewidth=3, label='n = 4')
ax.legend(loc=4)
ax.grid(); ax.set_title('Degree = 0 | Eq.(4.2.2)', fontweight='bold', fontsize=14)
```

Degree (m) = 1.

```
fig=plt.figure(figsize=(10, 6))
ax=fig.gca()
ax.plot(x, P1n[0, :], linewidth=3, label='n = 1')
ax.plot(x, P1n[1, :], linewidth=3, label='n = 2')
ax.plot(x, P1n[2, :], linewidth=3, label='n = 3')
ax.plot(x, P1n[3, :], linewidth=3, label='n = 4')
ax.plot(x, P1n[4, :], linewidth=3, label='n = 5')
ax.legend(loc=1)
ax.grid(); ax.set_title('Degree = 1 | Eq.(4.2.2)', fontweight='bold', fontsize=14)
```

Degree (m) = 0.

```
fig=plt.figure(figsize=(10, 6))
ax=fig.gca()
ax.plot(x, P2n[0, :], linewidth=3, label='n = 2')
ax.plot(x, P2n[1, :], linewidth=3, label='n = 3')
ax.plot(x, P2n[2, :], linewidth=3, label='n = 4')
ax.plot(x, P2n[3, :], linewidth=3, label='n = 5')
ax.plot(x, P2n[4, :], linewidth=3, label='n = 6')
ax.legend(loc=4)
ax.grid(); ax.set_title('Degree = 2 | Eq.(4.2.2)', fontweight='bold', fontsize=14)
```

# Task IV: Spectral methods solving PDE

We are now trying to solve *viscous Burgers' equation*, which is a simplified version of *Navier-Stokes equation*:

\begin{equation*}
    \frac{\partial T}{\partial t} + U_0 \frac{\partial T}{\partial x} = K \frac{\partial^2 T}{\partial x^2}
\end{equation*}

Here $U_0$ and $K$ are constants, so *Burgers' equation* is linearized.

It has a wave-like solution, in Fourier transform with wavenumber $m$, it is:
    $$
    T = \mathcal F\left( \hat{T}e^{i\xi x} \right), \qquad \xi = \frac{2m\pi}{L}
    $$

So we replace each $x$-derivative by $i \xi$ and replace $T$ by $\hat{T}$:

$$
    \hat{T_t} + i \xi U_0 \hat{T} = -\xi^2 K \hat{T}
$$
It is the ODE required by this homework.

The ODE above can be temporally discretizated by iterative methods (e.g. R.-K. method).

## Numerical scheme

Here I also provide a numerical solution for the ODE above:

* **Domain**: one-dimensional, 64 points, $x\ \epsilon\ [-\pi, \pi]$.
* **Initial condition**: $T(t=0) = 25^\circ C \cdot sin(x)^2$ for x < 0 only.
* **Temporal discretization**: 4th order Runge-Kutta method.
* **Fourier transform**: one-dimensional discrete Fourier Transform (in `numpy.fft`).

Here I multiplying all the terms by an integrating factor $\exp(\xi^2 K t)$:
$$
    \quad \\
    e^{\xi^2 K t} \hat{T}_t + i \xi U_0 e^{\xi^2 K t} \hat{T} = -\xi^2 K e^{\xi^2 K t}\hat{T}
$$

Then define $\hat{U} = e^{\xi^2 K t} \hat{T}$:
$$
    \hat{U}_t = \xi^2 K \hat{U} + e^{\xi^2 K t} \hat{T}_t \\
    \hat{U}_t - \xi^2 K \hat{U}  + i \xi U_0 \hat{U} = -\xi^2 K \hat{U} \\
    \quad
$$

Therefore, the dissipative term has been canceled out, and I actually solving:
$$
    \hat{U}_t = -i \xi U_0 \hat{U}
$$

```
# Set parameters
U0=0.5        # Advection coefficient
K=0.25        # Viscosity
m=64          # Number of grid points in 1-D
freq=1./m**2  # Time frequency
total_time=3. # maximum time
# Create domain
x=np.arange(-m/2,m/2)*(2*np.pi/m)
# All timestep
max_timestep=int(round(total_time/freq))
# Initial condition in frequency domain
T=25*np.sin(x)**2 * (x<0.)
U=np.fft.fft(T) # <------------------------------------------------ FFT
# Allocate result
result=np.empty([max_timestep+1, len(x)]) # result
result[0, :]=T
time_count=np.empty([max_timestep+1]) # absulote time
time_count[0]=0
# Coefficients in semi-discretization equation
xi=np.array([range(0, m/2) + [0] + range(-m/2+1, 0)]) # create xi
C_adv = -1j*freq*xi*U0 # coefficient of right-hand term
EXP = np.exp(-freq*K*xi**2/2) # integrating factor
# Solve ODE
for i in range(1, max_timestep+1):
    C1 = C_adv*U
    C2 = C_adv*EXP*(U+C1/2.)
    C3 = C_adv*EXP*U+C2/2
    C4 = C_adv*(EXP*U+EXP*C3)
    U = EXP*U + (EXP*C1+2*EXP*(C2+C3)+C4)/6. # <------------- Runge-Kutta
    # collecting results
    time_count[i]=i*freq
    result[i, :]=np.squeeze(np.real(np.fft.ifft(U))) # <----- Inverse FFT
```

## Solution & discussions

```
x_grid, t_grid = np.meshgrid(x, time_count[::500])
result_3Dplot = result[::500, :]
```

```
fig = plt.figure(figsize=(10, 6))
ax = fig.gca(projection='3d')
ax.plot_wireframe(x_grid, t_grid, result_3Dplot, rstride=2, cstride=100, color='k', linewidth=3, alpha=1)
ax.set_xlim3d(-np.pi, np.pi); ax.set_ylim3d(0, 3); ax.set_zlim3d(0, 25);
ax.set_xlabel('x grid', fontsize=12); ax.set_ylabel('time', fontsize=12); ax.set_zlabel('Temperature', fontsize=12)
ax.set_title('Solution in x-time frame', fontsize=14, fontweight='bold')
ax.view_init(20, -60)
```

```
fig, ax = plt.subplots(3, 3, figsize=(10, 10))
fig.tight_layout()
t_select=[0, 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3]
for l in range(3):
    for m in range(3):
        ax_temp=ax[l, m]; time_temp=t_select[3*l+m]
        ax_temp.plot(x, np.squeeze(result[time_count==time_temp, :]), color='k', linewidth=3)
        ax_temp.set_xlim([-np.pi, np.pi]); ax_temp.set_ylim([0,25])
        ax_temp.set_title('time='+str(time_temp), fontsize=12)
        ax_temp.grid()

```

From the figures above, we see that:

* Temperature pattern becomes "flat" during the time which shows the effect of dissipative term (viscosity). We can predict that, the temperature will become completely flat as time goes by.
* High-temperature regions and low-temperature regions move in the same speed (no skewness), which is different from traditional Burgers' equation. This is due to the constant $U_0$ in our equation.


# $H_{\rm SO}$, up to and including 3.5 post-Newtonian order

## This notebook constructs the spin-orbit coupling terms in the Hamiltonian up to 3.5 post-Newtonian order

**Notebook Status:**  Validated

**Validation Notes:** All expressions in this notebook were transcribed twice by hand on separate occasions, and expressions were corrected as needed to ensure consistency with published PN expressions. In addition, this tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). **Additional validation tests may have been performed, but are as yet, undocumented.**

## Author: Zach Etienne

### This notebook exists as the following Python module:
1. [PN_Hamiltonian_SO.py](../../edit/NRPyPN/PN_Hamiltonian_SO.py)

### This notebook & corresponding Python module depend on the following NRPy+/NRPyPN Python modules:
1. [indexedexp.py](../../edit/indexedexp.py): [**documentation+tutorial**](../Tutorial-Indexed_Expressions.ipynb)
1. [NRPyPN_shortcuts.py](../../edit/NRPyPN/NRPyPN_shortcuts.py): [**documentation**](NRPyPN_shortcuts.ipynb)

# Table of Contents
$$\label{toc}$$

1. Part 1: [$H_{\rm SO, 1.5PN}$](#onept5pn), as summarized in [Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048) (see references therein for sources)
1. Part 2: [$H_{\rm SO, 2.5PN}$](#twopt5pn), as derived by [Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048)
1. Part 3: [$H_{\rm SO, 3.5PN}$](#threept5pn), as derived in [Hartung and Steinhoff (2011)](https://arxiv.org/abs/1104.3079)
1. Part 4: [Validation against second transcription and corresponding Python module](#code_validation)
1. Part 5: [LaTeX PDF output](#latex_pdf_output): $\LaTeX$ PDF Output

# Part 1: $H_{\rm SO, 1.5PN}$, as summarized in [Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048) (see references therein for sources) \[Back to [top](#toc)\]
$$\label{onept5pn}$$

As described in the [nonspinning Hamiltonian notebook](PN-Hamiltonian-Nonspinning.ipynb), the basic physical system assumes two point particles of mass $m_1$ and $m_2$ with corresponding momentum vectors $\mathbf{P}_1$ and $\mathbf{P}_2$, and displacement vectors $\mathbf{X}_1$ and $\mathbf{X}_2$ with respect to the center of mass. Here we also consider the spin vectors of each point mass $\mathbf{S}_1$ and $\mathbf{S}_2$, respectively.

[Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048) adopt the notation

\begin{align}
\mathbf{r}_{12} &= (\mathbf{X}_1-\mathbf{X}_2)\\
r_{12} = r_{21} &= |\mathbf{r}_{12}|\\
\mathbf{n}_{12} &= \frac{\mathbf{r}_{12}}{r_{12}},
\end{align}
and when the numbers in subscripts are flipped, the particles are interchanged.

The spin-orbit terms of the Hamiltonian up to and including 3.5 PN order are generally given by:

$$
H_{\rm SO} = \mathbf{\Omega}_1 S^i_1 + \mathbf{\Omega}_2 S^i_2,
$$
where we need only define $\mathbf{\Omega}_1$ within a function, as $\mathbf{\Omega}_2$ is defined simply by interchanging $1\leftrightarrow 2$ in the $\mathbf{\Omega}_1$ expression.

At 1.5PN order (as summarized in [Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048), Eq 4.11a), we have

$$
\mathbf{\Omega}_{1,SO,1.5PN} = \frac{1}{r_{12}^2}\bigg( \frac{3m_2}{2m_1}{\mathbf{n}}_{12}\times{\mathbf{p}}_1 - 2 {\mathbf{n}}_{12}\times{\mathbf{p}}_2 \bigg),
$$

```python
# Step 0: Add NRPy's directory to the path
# https://stackoverflow.com/questions/16780014/import-file-from-parent-directory
import os, sys  # Standard Python modules for multiplatform OS-level functions
import indexedexpNRPyPN as ixp                    # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
from NRPyPN_shortcuts import div,dot,cross  # NRPyPN: shortcuts for e.g., vector operations

# 1.5PN spin-orbit coupling term, from Eq. 4.11a of
#    Damour, Jaranowski, and Schäfer (2008)
#      https://arxiv.org/abs/0711.1048
def f_H_SO_1p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, r12):
    def f_Omega1(m1,m2, n12U, p1U,p2U, r12):
        Omega1 = ixp.zerorank1()
        for i in range(3):
            Omega1[i] = (div(3,2)*m2/m1 * cross(n12U,p1U)[i] - 2*cross(n12U,p2U)[i])/r12**2
        return Omega1
    global H_SO_1p5PN
    Omega1 = f_Omega1(m1,m2, n12U, p1U,p2U, r12)
    Omega2 = f_Omega1(m2,m1, n21U, p2U,p1U, r12)
    H_SO_1p5PN = dot(Omega1,S1U) + dot(Omega2,S2U)
```

```python
# Second version, used for validation purposes only.
def f_H_SO_1p5PNv2(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, r12):
    def f_Omega_SO_1p5PN(m1,m2, n12U, p1U,p2U, r12):
        Omega1U = ixp.zerorank1()
        for i in range(3):
            Omega1U[i] = (div(3,2)*m2/m1 * cross(n12U,p1U)[i] - 2*cross(n12U,p2U)[i])/r12**2
        return Omega1U

    Omega1_1p5PNU = f_Omega_SO_1p5PN(m1,m2, n12U, p1U,p2U, r12)
    Omega2_1p5PNU = f_Omega_SO_1p5PN(m2,m1, n21U, p2U,p1U, r12)
    global H_SO_1p5PNv2
    H_SO_1p5PNv2 = dot(Omega1_1p5PNU,S1U) + dot(Omega2_1p5PNU,S2U)
```

# Part 2: $H_{\rm SO, 2.5PN}$, as derived by [Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048) \[Back to [top](#toc)\]
$$\label{twopt5pn}$$

To reduce possibility of copying error, equations are taken directly from the arXiv LaTeX source code of Eq 4.11b in [Damour, Jaranowski, and Schäfer (2008)](https://arxiv.org/abs/0711.1048), and only mildly formatted to (1) improve presentation in Jupyter notebooks and (2) to ensure some degree of consistency in notation across different terms in other Hamiltonian notebooks:

\begin{align}
\mathbf{\Omega}^{\rm NLO}_{1} &= \frac{G^2}{c^4r_{12}^3} \Bigg(
\bigg(-\frac{11}{2}m_2-5\frac{m_2^2}{m_1}\bigg){\mathbf{n}}_{12}\times{\mathbf{p}}_1
+ \bigg(6m_1+\frac{15}{2}m_2\bigg){\mathbf{n}}_{12}\times{\mathbf{p}}_2 \Bigg)\\
&\quad
+ \frac{G}{c^4r_{12}^2} \Bigg( \bigg(
- \frac{5m_2{\bf p}_1^2}{8m_1^3} - \frac{3({\mathbf{p}}_1\cdot{\mathbf{p}}_2)}{4m_1^2}
+ \frac{3{\bf p}_2^2}{4m_1m_2}
- \frac{3(\mathbf{n}_{12}\cdot\mathbf{p}_1)(\mathbf{n}_{12}\cdot\mathbf{p}_2)}{4m_1^2} - \frac{3(\mathbf{n}_{12}\cdot\mathbf{p}_2)^2}{2m_1m_2} \bigg){\mathbf{n}}_{12}\times{\mathbf{p}}_1
\\
&\quad\quad\quad\quad + \bigg(\frac{({\mathbf{p}}_1\cdot{\mathbf{p}}_2)}{m_1m_2}+\frac{3(\mathbf{n}_{12}\cdot\mathbf{p}_1)(\mathbf{n}_{12}\cdot\mathbf{p}_2)}{m_1m_2}\bigg){\mathbf{n}}_{12}\times{\mathbf{p}}_2
+ \bigg( \frac{3(\mathbf{n}_{12}\cdot\mathbf{p}_1)}{4m_1^2} - \frac{2(\mathbf{n}_{12}\cdot\mathbf{p}_2)}{m_1m_2} \bigg){\mathbf{p}}_1\times{\mathbf{p}}_2
\Bigg).
\end{align}

```python
# 2.5PN spin-orbit coupling term, from Eq. 4.11b of
#    Damour, Jaranowski, and Schäfer (2008)
#      https://arxiv.org/abs/0711.1048
def f_H_SO_2p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, r12):
    def f_Omega_SO_2p5PN(m1,m2, n12U, p1U,p2U, r12):
        Omega1 = ixp.zerorank1()
        for i in range(3):
            Omega1[i] = (+(+(-div(11,2)*m2-5*m2**2/m1)*cross(n12U,p1U)[i]
                           +(6*m1 + div(15,2)*m2)     *cross(n12U,p2U)[i])/r12**3
                         +(+(-div(5,8)*m2*dot(p1U,p1U)/m1**3
                             -div(3,4)*dot(p1U,p2U)/m1**2
                             +div(3,4)*dot(p2U,p2U)/(m1*m2)
                             -div(3,4)*dot(n12U,p1U)*dot(n12U,p2U)/m1**2
                             -div(3,2)*dot(n12U,p2U)**2/(m1*m2))*cross(n12U,p1U)[i]
                           +(dot(p1U,p2U)/(m1*m2) + 3*dot(n12U,p1U)*dot(n12U,p2U)/(m1*m2))*cross(n12U,p2U)[i]
                           +(div(3,4)*dot(n12U,p1U)/m1**2 - 2*dot(n12U,p2U)/(m1*m2))*cross(p1U,p2U)[i])/r12**2)
        return Omega1

    Omega1_2p5PNU = f_Omega_SO_2p5PN(m1,m2, n12U, p1U,p2U, r12)
    Omega2_2p5PNU = f_Omega_SO_2p5PN(m2,m1, n21U, p2U,p1U, r12)

    global H_SO_2p5PN
    H_SO_2p5PN = dot(Omega1_2p5PNU,S1U) + dot(Omega2_2p5PNU,S2U)
```

```python
# Second version, used for validation purposes only.
def f_H_SO_2p5PNv2(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, r12):
    def f_Omega_SO_2p5PNv2(m1,m2, n12U, p1U,p2U, r12):
        Omega1 = ixp.zerorank1()
        n12_cross_p1 = cross(n12U,p1U)
        n12_cross_p2 = cross(n12U,p2U)
        for i in range(3):
            Omega1[i] = ( (-div(11,2)*m2 - 5*m2**2/m1)*n12_cross_p1[i] +         # line 1
                          (6*m1 + div(15,2)*m2)       *n12_cross_p2[i] ) / r12**3  # line 1
            Omega1[i]+= (( -div(5,8)*m2*dot(p1U,p1U)/m1**3                          # line 2
                           -div(3,4)*dot(p1U,p2U)/m1**2                             # line 2
                           +div(3,4)*dot(p2U,p2U)/(m1*m2)                           # line 2
                           -div(3,4)*dot(n12U,p1U)*dot(n12U,p2U)/m1**2              # line 2
                           -div(3,2)*dot(n12U,p2U)**2/(m1*m2) )*n12_cross_p1[i] +   # line 2
                          ( dot(p1U,p2U)/(m1*m2) + 3*dot(n12U,p1U)*dot(n12U,p2U)/(m1*m2) )*n12_cross_p2[i] +  # line 3
                          (+div(3,4)*dot(n12U,p1U)/m1**2 - 2*dot(n12U,p2U)/(m1*m2) )*cross(p1U,p2U)[i] )/r12**2 # line 3
        return Omega1

    Omega1_2p5PNU = f_Omega_SO_2p5PNv2(m1,m2, n12U, p1U,p2U, r12)
    Omega2_2p5PNU = f_Omega_SO_2p5PNv2(m2,m1, n21U, p2U,p1U, r12)
    global H_SO_2p5PNv2
    H_SO_2p5PNv2 = dot(Omega1_2p5PNU,S1U) + dot(Omega2_2p5PNU,S2U)
```

# Part 3: $H_{\rm SO, 3.5PN}$, as derived in [Hartung and Steinhoff (2011)](https://arxiv.org/abs/1104.3079) \[Back to [top](#toc)\]
$$\label{threept5pn}$$

To reduce possibility of copying error, equations are taken directly from the arXiv LaTeX source code of Eq 5 in [Hartung and Steinhoff (2011)](https://arxiv.org/abs/1104.3079), and only mildly formatted to (1) improve presentation in Jupyter notebooks and (2) to ensure some degree of consistency in notation across different terms in other Hamiltonian notebooks:

\begin{align}
 H^{\text{NNLO}}_{\text{SO}} & = \frac{1}{r_{12}^2} \biggl[
	\biggl(
		\frac{7 m_2 (\mathbf{P}_1^2)^2}{16 m_1^5}
		+ \frac{9 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_1^2}{16 m_1^4}
		+ \frac{3 \mathbf{P}_1^2 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{4 m_1^3 m_2}\nonumber\\
&\quad\quad\quad		+ \frac{45 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)^3}{16 m_1^2 m_2^2}
		+ \frac{9 \mathbf{P}_1^2 (\mathbf{P}_1\cdot\mathbf{P}_2)}{16 m_1^4}
		- \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2 (\mathbf{P}_1\cdot\mathbf{P}_2)}{16 m_1^2 m_2^2}\nonumber\\
&\quad\quad\quad		- \frac{3 (\mathbf{P}_1^2) (\mathbf{P}_2^2)}{16 m_1^3 m_2}
		- \frac{15 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2) \mathbf{P}_2^2}{16 m_1^2 m_2^2}
		+ \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2 \mathbf{P}_2^2}{4 m_1 m_2^3}\nonumber\\
&\quad\quad\quad		- \frac{3 (\mathbf{P}_1\cdot\mathbf{P}_2) \mathbf{P}_2^2}{16 m_1^2 m_2^2}
		- \frac{3 (\mathbf{P}_2^2)^2}{16 m_1 m_2^3}
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_1)\mathbf{S}_1)\\
&\quad\quad\quad	+\biggl(
		- \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_1^2}{2 m_1^3 m_2}\nonumber\\
&\quad\quad\quad		- \frac{15 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2(\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{4 m_1^2 m_2^2}
		+ \frac{3 \mathbf{P}_1^2 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{4 m_1^2 m_2^2}
		- \frac{\mathbf{P}_1^2 (\mathbf{P}_1\cdot\mathbf{P}_2)}{2 m_1^3 m_2}
		+ \frac{(\mathbf{P}_1\cdot\mathbf{P}_2)^2}{2 m_1^2 m_2^2}\nonumber\\
&\quad\quad\quad		+ \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2 \mathbf{P}_2^2}{4 m_1^2 m_2^2}
		- \frac{(\mathbf{P}_1^2) (\mathbf{P}_2^2)}{4 m_1^2 m_2^2}
		- \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_2^2}{2 m_1 m_2^3}\nonumber\\
&\quad\quad\quad		- \frac{(\mathbf{P}_1\cdot\mathbf{P}_2) \mathbf{P}_2^2}{2 m_1 m_2^3}
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_2)\mathbf{S}_1)\\
&\quad\quad\quad	+\biggl(
		- \frac{9 (\mathbf{n}_{12}\cdot\mathbf{P}_1) \mathbf{P}_1^2}{16 m_1^4}
		+ \frac{\mathbf{P}_1^2 (\mathbf{n}_{12}\cdot\mathbf{P}_2)}{m_1^3 m_2}\nonumber\\
&\quad\quad\quad		+ \frac{27 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{16 m_1^2 m_2^2}
		- \frac{(\mathbf{n}_{12}\cdot\mathbf{P}_2)(\mathbf{P}_1\cdot\mathbf{P}_2)}{8 m_1^2 m_2^2}
		-\frac{5 (\mathbf{n}_{12}\cdot\mathbf{P}_1) \mathbf{P}_2^2}{16 m_1^2 m_2^2}\nonumber\\
&\quad\quad\quad		+ \frac{(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_2^2}{m_1 m_2^3}
	\biggr)((\mathbf{P}_1 \times \mathbf{P}_2)\mathbf{S}_1)
\biggr] \nonumber\\
&+ \frac{1}{r_{12}^3} \biggl[
	\biggl(
		-\frac{3 m_2 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2}{2 m_1^2}
		+\left(
			-\frac{3 m_2}{2 m_1^2}
			+\frac{27 m_2^2}{8 m_1^3}
		\right) \mathbf{P}_1^2
		+\left(
			\frac{177}{16 m_1}
			+\frac{11}{m_2}
		\right) (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2\nonumber\\
&\quad\quad\quad		+\left(
			\frac{11}{2 m_1}
			+\frac{9 m_2}{2 m_1^2}
		\right) (\mathbf{n}_{12}\cdot\mathbf{P}_1) (\mathbf{n}_{12}\cdot\mathbf{P}_2)
		+\left(
			\frac{23}{4 m_1}
			+\frac{9 m_2}{2 m_1^2}
		\right) (\mathbf{P}_1\cdot\mathbf{P}_2)\nonumber\\
&\quad\quad\quad		-\left(
			\frac{159}{16 m_1}
			+\frac{37}{8 m_2}
		\right) \mathbf{P}_2^2
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_1)\mathbf{S}_1)
	+\biggl(
		\frac{4 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2}{m_1}
		+\frac{13 \mathbf{P}_1^2}{2 m_1}\nonumber\\
&\quad\quad\quad		+\frac{5 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{m_2}
		+\frac{53 \mathbf{P}_2^2}{8 m_2}
		- \left(
			\frac{211}{8 m_1}
			+\frac{22}{m_2}
		\right) (\mathbf{n}_{12}\cdot\mathbf{P}_1) (\mathbf{n}_{12}\cdot\mathbf{P}_2)\nonumber\\
&\quad\quad\quad		-\left(
			\frac{47}{8 m_1}
			+\frac{5}{m_2}
		\right)(\mathbf{P}_1\cdot\mathbf{P}_2)
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_2)\mathbf{S}_1)
	+\biggl(
		-\left(
			\frac{8}{m_1}
			+\frac{9 m_2}{2 m_1^2}
		\right)(\mathbf{n}_{12}\cdot\mathbf{P}_1)\nonumber\\
&\quad\quad\quad		+\left(
			\frac{59}{4 m_1}
			+\frac{27}{2 m_2}
		\right)(\mathbf{n}_{12}\cdot\mathbf{P}_2)
	\biggr)((\mathbf{P}_1 \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]\nonumber\\
&+\frac{1}{r_{12}^4} \biggl[
	\left(
		\frac{181 m_1 m_2}{16}
		+ \frac{95 m_2^2}{4}
		+ \frac{75 m_2^3}{8 m_1}
	\right) ((\mathbf{n}_{12} \times \mathbf{P}_1)\mathbf{S}_1)\nonumber\\
&\quad\quad\quad	- \left(
		\frac{21 m_1^2}{2}
		+ \frac{473 m_1 m_2}{16}
		+ \frac{63 m_2^2}{4}
	\right)((\mathbf{n}_{12} \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]
 + (1\leftrightarrow2)\,.
\end{align}

Let's split the above into more bite-sized pieces. First:

\begin{align}
H^a_{SO,2.5PN} &= \frac{1}{r_{12}^2} \biggl[
	\biggl(
		\frac{7 m_2 (\mathbf{P}_1^2)^2}{16 m_1^5}
		+ \frac{9 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_1^2}{16 m_1^4}
		+ \frac{3 \mathbf{P}_1^2 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{4 m_1^3 m_2}\nonumber\\
&\quad\quad\quad		+ \frac{45 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)^3}{16 m_1^2 m_2^2}
		+ \frac{9 \mathbf{P}_1^2 (\mathbf{P}_1\cdot\mathbf{P}_2)}{16 m_1^4}
		- \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2 (\mathbf{P}_1\cdot\mathbf{P}_2)}{16 m_1^2 m_2^2}\nonumber\\
&\quad\quad\quad		- \frac{3 (\mathbf{P}_1^2) (\mathbf{P}_2^2)}{16 m_1^3 m_2}
		- \frac{15 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2) \mathbf{P}_2^2}{16 m_1^2 m_2^2}
		+ \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2 \mathbf{P}_2^2}{4 m_1 m_2^3}\nonumber\\
&\quad\quad\quad		- \frac{3 (\mathbf{P}_1\cdot\mathbf{P}_2) \mathbf{P}_2^2}{16 m_1^2 m_2^2}
		- \frac{3 (\mathbf{P}_2^2)^2}{16 m_1 m_2^3}
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_1)\mathbf{S}_1)\biggr]
\end{align}

```python
# 3.5PN spin-orbit coupling term, from Eq. 5 of
#    Hartung and Steinhoff (2011)
#   https://arxiv.org/abs/1104.3079

# 3.5PN H_SO:  Omega_1, part 1:
def HS2011_Omega_SO_3p5PN_pt1(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ((+7*m2*dot(p1U,p1U)**2/(16*m1**5)
                      +9*dot(n12U,p1U)*dot(n12U,p2U)*dot(p1U,p1U)/(16*m1**4)
                      +3*dot(p1U,p1U)*dot(n12U,p2U)**2/(4*m1**3*m2)
                      +45*dot(n12U,p1U)*dot(n12U,p2U)**3/(16*m1**2*m2**2)
                      +9*dot(p1U,p1U)*dot(p1U,p2U)/(16*m1**4)
                      -3*dot(n12U,p2U)**2*dot(p1U,p2U)/(16*m1**2*m2**2)
                      -3*dot(p1U,p1U)*dot(p2U,p2U)/(16*m1**3*m2)
                      -15*dot(n12U,p1U)*dot(n12U,p2U)*dot(p2U,p2U)/(16*m1**2*m2**2)
                      +3*dot(n12U,p2U)**2*dot(p2U,p2U)/(4*m1*m2**3)
                      -3*dot(p1U,p2U)*dot(p2U,p2U)/(16*m1**2*m2**2)
                      -3*dot(p2U,p2U)**2/(16*m1*m2**3))*cross(n12U,p1U)[i])/r12**2
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt1v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( (+div(7,16)*m2*dot(p1U,p1U)**2/m1**5
                       +div(9,16)*dot(n12U,p1U)*dot(n12U,p2U)*dot(p1U,p1U)/m1**4
                       +div(3,4) *dot(p1U,p1U)*dot(n12U,p2U)**2/(m1**3*m2)
                       +div(45,16)*dot(n12U,p1U)*dot(n12U,p2U)**3/(m1**2*m2**2)
                       +div(9,16)*dot(p1U,p1U)*dot(p1U,p2U)/m1**4
                       -div(3,16)*dot(n12U,p2U)**2*dot(p1U,p2U)/(m1**2*m2**2)
                       -div(3,16)*dot(p1U,p1U)*dot(p2U,p2U)/(m1**3*m2)
                       -div(15,16)*dot(n12U,p1U)*dot(n12U,p2U)*dot(p2U,p2U)/(m1**2*m2**2)
                       +div(3,4)*dot(n12U,p2U)**2*dot(p2U,p2U)/(m1*m2**3)
                       -div(3,16)*dot(p1U,p2U)*dot(p2U,p2U)/(m1**2*m2**2)
                       -div(3,16)*dot(p2U,p2U)**2/(m1*m2**3)) * cross(n12U,p1U)[i] )/q**2
    return Omega1
```

Next,

\begin{align}
H^b_{SO,2.5PN} &= \frac{1}{r_{12}^2} \biggl[
+\biggl(
- \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_1^2}{2 m_1^3 m_2}\nonumber\\
&\quad\quad\quad		- \frac{15 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2(\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{4 m_1^2 m_2^2}
+ \frac{3 \mathbf{P}_1^2 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{4 m_1^2 m_2^2}
- \frac{\mathbf{P}_1^2 (\mathbf{P}_1\cdot\mathbf{P}_2)}{2 m_1^3 m_2}
+ \frac{(\mathbf{P}_1\cdot\mathbf{P}_2)^2}{2 m_1^2 m_2^2}\nonumber\\
&\quad\quad\quad		+ \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2 \mathbf{P}_2^2}{4 m_1^2 m_2^2}
- \frac{(\mathbf{P}_1^2) (\mathbf{P}_2^2)}{4 m_1^2 m_2^2}
- \frac{3 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_2^2}{2 m_1 m_2^3}\nonumber\\
&\quad\quad\quad		- \frac{(\mathbf{P}_1\cdot\mathbf{P}_2) \mathbf{P}_2^2}{2 m_1 m_2^3}
\biggr)((\mathbf{n}_{12} \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]
\end{align}

```python
# 3.5PN H_SO:  Omega_1, part 2:
def HS2011_Omega_SO_3p5PN_pt2(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = (+(-3*dot(n12U,p1U)*dot(n12U,p2U)*dot(p1U,p1U)/(2*m1**3*m2)
                       -15*dot(n12U,p1U)**2*dot(n12U,p2U)**2/(4*m1**2*m2**2)
                       +3*dot(p1U,p1U)*dot(n12U,p2U)**2/(4*m1**2*m2**2)
                       -dot(p1U,p1U)*dot(p1U,p2U)/(2*m1**3*m2)
                       +dot(p1U,p2U)**2/(2*m1**2*m2**2)
                       +3*dot(n12U,p1U)**2*dot(p2U,p2U)/(4*m1**2*m2**2)
                       -dot(p1U,p1U)*dot(p2U,p2U)/(4*m1**2*m2**2)
                       -3*dot(n12U,p1U)*dot(n12U,p2U)*dot(p2U,p2U)/(2*m1*m2**3)
                       -dot(p1U,p2U)*dot(p2U,p2U)/(2*m1*m2**3))*cross(n12U,p2U)[i])/r12**2
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt2v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( (-div(3,2)*dot(n12U,p1U)*dot(n12U,p2U)*dot(p1U,p1U)/(m1**3*m2)
                       -div(15,4)*dot(n12U,p1U)**2*dot(n12U,p2U)**2/(m1**2*m2**2)
                       +div(3,4)*dot(p1U,p1U)*dot(n12U,p2U)**2/(m1**2*m2**2)
                       -div(1,2)*dot(p1U,p1U)*dot(p1U,p2U)/(m1**3*m2)
                       +div(1,2)*dot(p1U,p2U)**2/(m1**2*m2**2)
                       +div(3,4)*dot(n12U,p1U)**2*dot(p2U,p2U)/(m1**2*m2**2)
                       -div(1,4)*dot(p1U,p1U)*dot(p2U,p2U)/(m1**2*m2**2)
                       -div(3,2)*dot(n12U,p1U)*dot(n12U,p2U)*dot(p2U,p2U)/(m1*m2**3)
                       -div(1,2)*dot(p1U,p2U)*dot(p2U,p2U)/(m1*m2**3))*cross(n12U,p2U)[i] )/q**2
    return Omega1
```

Part 3:

\begin{align}
H^c_{SO,2.5PN} &= \frac{1}{r_{12}^2} \biggl[
+\biggl(
		- \frac{9 (\mathbf{n}_{12}\cdot\mathbf{P}_1) \mathbf{P}_1^2}{16 m_1^4}
		+ \frac{\mathbf{P}_1^2 (\mathbf{n}_{12}\cdot\mathbf{P}_2)}{m_1^3 m_2}\nonumber\\
&\quad\quad\quad		+ \frac{27 (\mathbf{n}_{12}\cdot\mathbf{P}_1)(\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{16 m_1^2 m_2^2}
		- \frac{(\mathbf{n}_{12}\cdot\mathbf{P}_2)(\mathbf{P}_1\cdot\mathbf{P}_2)}{8 m_1^2 m_2^2}
		-\frac{5 (\mathbf{n}_{12}\cdot\mathbf{P}_1) \mathbf{P}_2^2}{16 m_1^2 m_2^2}\nonumber\\
&\quad\quad\quad		+ \frac{(\mathbf{n}_{12}\cdot\mathbf{P}_2)\mathbf{P}_2^2}{m_1 m_2^3}
	\biggr)((\mathbf{P}_1 \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]
\end{align}

```python
# 3.5PN H_SO:  Omega_1, part 3:
def HS2011_Omega_SO_3p5PN_pt3(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = (+(-9*dot(n12U,p1U)*dot(p1U,p1U)/(16*m1**4)
                       +dot(p1U,p1U)*dot(n12U,p2U)/(m1**3*m2)
                       +27*dot(n12U,p1U)*dot(n12U,p2U)**2/(16*m1**2*m2**2)
                       -dot(n12U,p2U)*dot(p1U,p2U)/(8*m1**2*m2**2)
                       -5*dot(n12U,p1U)*dot(p2U,p2U)/(16*m1**2*m2**2))*cross(p1U,p2U)[i])/r12**2
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt3v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( (-div(9,16)*dot(n12U,p1U)*dot(p1U,p1U)/m1**4
                       +          dot(p1U,p1U)*dot(n12U,p2U)/(m1**3*m2)
                       +div(27,16)*dot(n12U,p1U)*dot(n12U,p2U)**2/(m1**2*m2**2)
                       -div(1,8)*dot(n12U,p2U)*dot(p1U,p2U)/(m1**2*m2**2)
                       -div(5,16)*dot(n12U,p1U)*dot(p2U,p2U)/(m1**2*m2**2)
                       +          dot(n12U,p2U)*dot(p2U,p2U)/(m1*m2**3))*cross(p1U,p2U)[i] )/q**2
    return Omega1
```

Part 4, the first $1/r_{12}^3$ term:

\begin{align}
H^d_{SO,2.5PN} &= \frac{1}{r_{12}^3} \biggl[
	\biggl(
		-\frac{3 m_2 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2}{2 m_1^2}
		+\left(
			-\frac{3 m_2}{2 m_1^2}
			+\frac{27 m_2^2}{8 m_1^3}
		\right) \mathbf{P}_1^2
		+\left(
			\frac{177}{16 m_1}
			+\frac{11}{m_2}
		\right) (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2\nonumber\\
&\quad\quad\quad		+\left(
			\frac{11}{2 m_1}
			+\frac{9 m_2}{2 m_1^2}
		\right) (\mathbf{n}_{12}\cdot\mathbf{P}_1) (\mathbf{n}_{12}\cdot\mathbf{P}_2)
		+\left(
			\frac{23}{4 m_1}
			+\frac{9 m_2}{2 m_1^2}
		\right) (\mathbf{P}_1\cdot\mathbf{P}_2)\nonumber\\
&\quad\quad\quad		-\left(
			\frac{159}{16 m_1}
			+\frac{37}{8 m_2}
		\right) \mathbf{P}_2^2
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_1)\mathbf{S}_1)
\biggr]
\end{align}

```python
# 3.5PN H_SO:  Omega_1, part 4:
def HS2011_Omega_SO_3p5PN_pt4(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = (+(-3*m2*dot(n12U,p1U)**2/(2*m1**2)
                       +((-3*m2)/(2*m1**2) + 27*m2**2/(8*m1**3))*dot(p1U,p1U)
                       +(177/(16*m1) + 11/m2)*dot(n12U,p2U)**2
                       +(11/(2*m1) + 9*m2/(2*m1**2))*dot(n12U,p1U)*dot(n12U,p2U)
                       +(23/(4*m1) + 9*m2/(2*m1**2))*dot(p1U,p2U)
                       -(159/(16*m1) + 37/(8*m2))*dot(p2U,p2U))*cross(n12U,p1U)[i])/r12**3
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt4v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( (-div(3,2)*m2*dot(n12U,p1U)**2/m1**2
                       +(-div(3,2)*m2/m1**2 + div(27,8)*m2**2/m1**3)*dot(p1U,p1U)
                       +(+div(177,16)/m1 + 11/m2)*dot(n12U,p2U)**2
                       +(+div(11,2)/m1 + div(9,2)*m2/m1**2)*dot(n12U,p1U)*dot(n12U,p2U)
                       +(+div(23,4)/m1 + div(9,2)*m2/m1**2)*dot(p1U,p2U)
                       -(+div(159,16)/m1 + div(37,8)/m2)*dot(p2U,p2U) )*cross(n12U,p1U)[i] )/q**3
    return Omega1
```

Part 5, the second $1/r_{12}^3$ term:

\begin{align}
H^e_{SO,2.5PN} &= \frac{1}{r_{12}^3} \biggl[
	+\biggl(
		\frac{4 (\mathbf{n}_{12}\cdot\mathbf{P}_1)^2}{m_1}
		+\frac{13 \mathbf{P}_1^2}{2 m_1}\nonumber\\
&\quad\quad\quad		+\frac{5 (\mathbf{n}_{12}\cdot\mathbf{P}_2)^2}{m_2}
		+\frac{53 \mathbf{P}_2^2}{8 m_2}
		- \left(
			\frac{211}{8 m_1}
			+\frac{22}{m_2}
		\right) (\mathbf{n}_{12}\cdot\mathbf{P}_1) (\mathbf{n}_{12}\cdot\mathbf{P}_2)\nonumber\\
&\quad\quad\quad		-\left(
			\frac{47}{8 m_1}
			+\frac{5}{m_2}
		\right)(\mathbf{P}_1\cdot\mathbf{P}_2)
	\biggr)((\mathbf{n}_{12} \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]
\end{align}

```python
# 3.5PN H_SO:  Omega_1, part 5:
def HS2011_Omega_SO_3p5PN_pt5(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = (+(+4*dot(n12U,p1U)**2/m1
                       +13*dot(p1U,p1U)/(2*m1)
                       +5*dot(n12U,p2U)**2/m2
                       +53*dot(p2U,p2U)/(8*m2)
                       -(211/(8*m1) + 22/m2)*dot(n12U,p1U)*dot(n12U,p2U)
                       -(47/(8*m1) + 5/m2)*dot(p1U,p2U))*cross(n12U,p2U)[i])/r12**3
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt5v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( (+4*dot(n12U,p1U)**2/m1
                       +div(13,2)*dot(p1U,p1U)/m1
                       +5*dot(n12U,p2U)**2/m2
                       +div(53,8)*dot(p2U,p2U)/m2
                       -(div(211,8)/m1+22/m2)*dot(n12U,p1U)*dot(n12U,p2U)
                       -(div(47,8)/m1+5/m2)*dot(p1U,p2U)) * cross(n12U,p2U)[i] )/q**3
    return Omega1
```

Part 6, the third $1/r_{12}^3$ term:

\begin{align}
H^f_{SO,2.5PN} &= \frac{1}{r_{12}^3} \biggl[
	+\biggl(
		-\left(
			\frac{8}{m_1}
			+\frac{9 m_2}{2 m_1^2}
		\right)(\mathbf{n}_{12}\cdot\mathbf{P}_1)\nonumber\\
&\quad\quad\quad		+\left(
			\frac{59}{4 m_1}
			+\frac{27}{2 m_2}
		\right)(\mathbf{n}_{12}\cdot\mathbf{P}_2)
	\biggr)((\mathbf{P}_1 \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]
\end{align}

```python
# 3.5PN H_SO:  Omega_1, part 6:
def HS2011_Omega_SO_3p5PN_pt6(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = (+(-(8/m1 + 9*m2/(2*m1**2))*dot(n12U,p1U)
                       +(59/(4*m1) + 27/(2*m2))*dot(n12U,p2U))*cross(p1U,p2U)[i])/r12**3
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt6v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( (-(        8/m1 + div(9,2)*m2/m1**2)*dot(n12U,p1U)
                       +(div(59,4)/m1 + div(27,2)/m2)     *dot(n12U,p2U))*cross(p1U,p2U)[i] )/q**3
    return Omega1
```

Finally part 7, the $1/r_{12}^4$ term:

\begin{align}
H^f_{SO,2.5PN} &= \frac{1}{r_{12}^4} \biggl[
	\left(
		\frac{181 m_1 m_2}{16}
		+ \frac{95 m_2^2}{4}
		+ \frac{75 m_2^3}{8 m_1}
	\right) ((\mathbf{n}_{12} \times \mathbf{P}_1)\mathbf{S}_1)\nonumber\\
&\quad\quad\quad	- \left(
		\frac{21 m_1^2}{2}
		+ \frac{473 m_1 m_2}{16}
		+ \frac{63 m_2^2}{4}
	\right)((\mathbf{n}_{12} \times \mathbf{P}_2)\mathbf{S}_1)
\biggr]
\end{align}

```python
# 3.5PN H_SO:  Omega_1, part 7:
def HS2011_Omega_SO_3p5PN_pt7(m1,m2, n12U, p1U,p2U, r12):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = (+(181*m1*m2/16 + 95*m2**2/4 + 75*m2**3/(8*m1))*cross(n12U,p1U)[i]
                     -(21*m1**2/2 + 473*m1*m2/16 + 63*m2**2/4)*cross(n12U,p2U)[i])/r12**4
    return Omega1
```

```python
# Second version, used for validation purposes only.
def HS2011_Omega_SO_3p5PN_pt7v2(m1,m2, n12U, p1U,p2U, q):
    Omega1 = ixp.zerorank1()
    for i in range(3):
        Omega1[i] = ( +(div(181,16)*m1*m2 + div(95,4)*m2**2   + div(75,8)*m2**3/m1)*cross(n12U,p1U)[i]
                      -(div(21,2)*m1**2   + div(473,16)*m1*m2 + div(63,4)*m2**2   )*cross(n12U,p2U)[i] )/q**4
    return Omega1
```

Now we put all the $\Omega$ terms together:

```python
def f_H_SO_3p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, r12):
    Omega1_3p5PNU = ixp.zerorank1()
    Omega2_3p5PNU = ixp.zerorank1()
    for i in range(3):
        Omega1_3p5PNU[i] = HS2011_Omega_SO_3p5PN_pt1(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt3(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt4(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt5(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt6(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt7(m1,m2, n12U, p1U,p2U, r12)[i]

        Omega2_3p5PNU[i] = HS2011_Omega_SO_3p5PN_pt1(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt3(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt4(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt5(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt6(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt7(m2,m1, n21U, p2U,p1U, r12)[i]

    global H_SO_3p5PN
    H_SO_3p5PN = dot(Omega1_3p5PNU,S1U) + dot(Omega2_3p5PNU,S2U)
```

```python
# For validation purposes only:
def f_H_SO_3p5PNv2(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, r12):
    Omega1_3p5PNU = ixp.zerorank1()
    Omega2_3p5PNU = ixp.zerorank1()
    for i in range(3):
        Omega1_3p5PNU[i] = HS2011_Omega_SO_3p5PN_pt1v2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt2v2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt3v2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt4v2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt5v2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt6v2(m1,m2, n12U, p1U,p2U, r12)[i]
        Omega1_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt7v2(m1,m2, n12U, p1U,p2U, r12)[i]

        Omega2_3p5PNU[i] = HS2011_Omega_SO_3p5PN_pt1v2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt2v2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt3v2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt4v2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt5v2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt6v2(m2,m1, n21U, p2U,p1U, r12)[i]
        Omega2_3p5PNU[i]+= HS2011_Omega_SO_3p5PN_pt7v2(m2,m1, n21U, p2U,p1U, r12)[i]

    global H_SO_3p5PNv2
    H_SO_3p5PNv2 = dot(Omega1_3p5PNU,S1U) + dot(Omega2_3p5PNU,S2U)
```

# Part 4: Validation against second transcription and corresponding Python module \[Back to [top](#toc)\]
$$\label{code_validation}$$

As a code validation check, we verify agreement between
* the SymPy expressions transcribed from the cited published work on two separate occasions, and
* the SymPy expressions generated in this notebook, and the corresponding Python module.

```python
from NRPyPN_shortcuts import m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q # Import needed input variables
import sympy as sp              # SymPy: The Python computer algebra package upon which NRPy+ depends

f_H_SO_1p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
f_H_SO_2p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
f_H_SO_3p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)

def error(varname):
    print("ERROR: When comparing Python module & notebook, "+varname+" was found not to match.")
    sys.exit(1)

# Validation against second transcription of the expressions:
f_H_SO_1p5PNv2(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
f_H_SO_2p5PNv2(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
f_H_SO_3p5PNv2(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
if sp.simplify(H_SO_1p5PN   - H_SO_1p5PNv2)   != 0: error("H_SO_1p5PNv2")
if sp.simplify(H_SO_2p5PN   - H_SO_2p5PNv2)   != 0: error("H_SO_2p5PNv2")
if sp.simplify(H_SO_3p5PN   - H_SO_3p5PNv2)   != 0: error("H_SO_3p5PNv2")

# Validation against corresponding Python module:
import PN_Hamiltonian_SO as HSO
HSO.f_H_SO_1p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
HSO.f_H_SO_2p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)
HSO.f_H_SO_3p5PN(m1,m2, n12U,n21U, S1U, S2U, p1U,p2U, q)

if sp.simplify(H_SO_1p5PN - HSO.H_SO_1p5PN)   != 0: error("H_SO_1p5PN")
if sp.simplify(H_SO_2p5PN - HSO.H_SO_2p5PN)   != 0: error("H_SO_2p5PN")
if sp.simplify(H_SO_3p5PN - HSO.H_SO_3p5PN)   != 0: error("H_SO_3p5PN")
print("ALL TESTS PASS")
```

    ALL TESTS PASS

# Part 5: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[PN-Hamiltonian-Spin-Orbit.pdf](PN-Hamiltonian-Spin-Orbit.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helperNRPyPN as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("PN-Hamiltonian-Spin-Orbit",location_of_template_file=os.path.join(".."))
```

    Created PN-Hamiltonian-Spin-Orbit.tex, and compiled LaTeX file to PDF file
        PN-Hamiltonian-Spin-Orbit.pdf