This jupyter notebook contains the code and numerical illustrations associated with our paper Flagfolds https://arxiv.org/abs/2305.10583 B. Buet, X. Pennec.
This code is distributed under the Licence CC-BY-NC
import numpy as np
from scipy import linalg
import matplotlib.pyplot as plt
Let $n \in \mathbb{N}$, $n \geq 2$. We introduce the notations: $\mathring{\Delta}(n) = \{ \mu = (\mu_1, \ldots, \mu_n) \in (0,1)^n \: : \: \mu_1 + \ldots + \mu_n = 1 \}$ and
$$ {\rm O}(n) = \{ U \in {\rm M}_n(\mathbb{R}) \: : \: U^T U = I_n \} \quad \text{and} \quad {\rm Sym}(n) = \{ A \in {\rm M}_n(\mathbb{R}) \: : \: A^T = A \} \quad \text{and} \quad {\rm Skew}(n) = \{ B \in {\rm M}_n(\mathbb{R}) \: : \: B^T = - B \} .$$We will represent $(\mu, U) \in \mathring{\Delta}(n) \times {\rm O}(n)$ by an ellipsoïd $\mathcal{Ell}(\mu,U)$ as follows. For $k \in \{1, \ldots, n\}$, we define $\lambda_k = \sum_{j=k}^n \frac{\mu_j}{j}$ and we denote by $u_k$ the $k^{th}$ column of $U$. Then, $\mathcal{Ell}(\mu,U)$ is the ellipsoïd of axes directed by $u_1, \ldots, u_n$ with semi-axis lengths $\sqrt{n \lambda_1}, \ldots, \sqrt{n \lambda_n}$.
When representing several ellipsoïds, we translate theirs centers so that they do not overlap.
from mpl_toolkits import mplot3d
def ellipsoid(coefs, R):
    # coefs = (a0, a1, a2)  # Coefficients in x**2/a0 + y**2/a1 + z**2/a2 = 1 
    # coefs = (n lambda_1, ..., n lambda_n)
    rx, ry, rz = np.sqrt(coefs)
    #print(coefs)
    # Set of all spherical angles:
    u = np.linspace(0, 2 * np.pi, 100)
    v = np.linspace(0, np.pi, 100)
    
    # Cartesian coordinates that correspond to the spherical angles:
    coord = np.zeros((3,100,100))
    coord[0] = rx * np.outer(np.cos(u), np.sin(v))
    coord[1] = ry * np.outer(np.sin(u), np.sin(v))
    coord[2] = rz * np.outer(np.ones_like(u), np.cos(v))
    #
    A = np.dot(R, np.reshape(coord,(3,100*100)))
    A = np.reshape(A,(3,100,100))
    #print(A.shape)
    
    return A[0], A[1], A[2]
x,y,z = ellipsoid(np.array([1,4,9]), np.eye(3))
%matplotlib notebook
ax = plt.axes(projection='3d')
ax.set_axis_off()
ell = ax.plot_surface(x, y, z,  rstride=4, cstride=4)
boxSize = 5
ax.set_xlim(-boxSize, boxSize) ; ax.set_ylim(-boxSize, boxSize) ; ax.set_zlim(-boxSize, boxSize)
ax.set_box_aspect((1,1,1))
plt.show()
We refer to Section 5 of the Arxiv version.
We are given $f : [0,1]^n \rightarrow [0, +\infty[$ satisfying $f(\mu) = 0$ if and only if $\mu = 0$ and we assume that $f$ is ${\rm C}^2$ in $[0,1]^n \setminus \{0\}$.
Given $1 \leq i < j \leq n$, we denote $\mu_{i \to j} = (0, \ldots, 0, \mu_i, \mu_{i+1}, \ldots,\mu_{j-1}, \mu_j, 0, \ldots, 0) \in [0,1]^n$.
We are interested in $(\mu, U) : I \subset \mathbb{R} \rightarrow \mathring{\Delta}(n) \times {\rm O}(n)$ solutions of the following differential equations:
$$ U^\prime = U B \quad \text{with } B = (b_{ij})_{ij} \in {\rm Skew(n)} \text{ and for } i<j, \, b_{ij} = \frac{1}{f(\mu_{i \to j})^2} \left( U^T U(0) C(0) U(0)^T U \right)_{ij} \quad \text{and} \quad C(0) = \left( f(\mu_{i \to j}(0))^2 b_{ij}(0) \right)_{ij} \: . $$and $$ \mu_k^{\prime\prime} = \frac{1}{n}\sum_{\substack{l = 1 \\ l \neq k}}^n \sum_{1 \leq i \leq l < j \leq n} f(\mu_{i \to j}) \: \partial_l f (\mu_{i \to j}) b_{ij}^2 - \frac{n-1}{n} \sum_{1 \leq i \leq k < j \leq n} f(\mu_{i \to j}) \: \partial_k f (\mu_{i \to j}) b_{ij}^2 \quad \text{for } k \in \{1, \ldots, n\} \: . $$
Let us discretize $[0,1]$ with $0 = t_0 < t_1 < \ldots < t_N = 1$ uniformly: for all $p$, $t_{p+1} - t_p = h = \frac{1}{N}$. We propose the following numerical scheme to compute $(\mu^{(p)}, U^{(p)})_p$ in $\mathring{\Delta}(n) \times {\rm O}(n)$:
Initial data are $(\mu^{(0)}, U^{(0)}) \in \mathring{\Delta}(n) \times {\rm O}(n)$ and $(\mu^\prime)^{(0)} \in \{ x = (x_1, \ldots, x_n) \in \mathbb{R}^n \: : \: x_1 + \ldots + x_n = 0 \}$, $B^{(0)} \in {\rm Skew(n)}$.
For $p \in \{0, \ldots, N-1\}$,
$$ \begin{align} b_{ij}^{(p)} & = \frac{1}{f(\mu_{i \to j}^{(p)})^2} \left( (U^{(p)})^T U^{(0)} C^{(0)} (U^{(0)})^T U^{(p)} \right)_{ij} \quad \text{where} \quad C^{(0)} = \left( f(\mu_{i \to j}^{(0)})^2 b_{ij}^{(0)} \right)_{ij} \tag{$1$} \\ \mu^{(p+1)} & = \mu^{(p)} + h(\mu^\prime)^{(p)} \tag{$2$} \\ U^{(p+1)} & = U^{(p)} \exp \left(h B^{(p)} \right) \tag{$3$}\\ (\mu^\prime_k)^{(p+1)} & = (\mu^\prime_k)^{(p)} + \frac{h}{n} \left( \sum_{l = 1}^n \sum_{1 \leq i \leq l < j \leq n} f(\mu_{i \to j}^{(p)}) \: \partial_l f (\mu_{i \to j}^{(p)}) (b_{ij}^{(p)})^2 - n \sum_{1 \leq i \leq k < j \leq n} f(\mu_{i \to j}^{(p)}) \: \partial_k f (\mu_{i \to j}^{(p)}) (b_{ij}^{(p)})^2 \right) \quad \text{for } k \in \{1, \ldots, n\} \tag{$4$} \end{align} $$We test the implementation with $f(\nu) = \frac14 |\nu|$ so that for all $k \in \{1, \ldots, n\}$ and $\nu \neq 0$, $\partial_k f(\nu) = \frac14 \frac{\nu_k}{|\nu|}$.
It is possible to change the expression of $f$, testing for instance $f(\nu) = \nu_1 + \ldots + \nu_n$.
We first define $f$ and its partial derivatives $\partial_k f$.
def f(nu):
    return 0.25*np.linalg.norm(nu)
    #return np.sum(nu)
def partialf(nu,k):
    return 0.25*nu[k-1]/np.linalg.norm(nu)
    #return 1.
We then define
troncNuItoJ $: (\nu, i, j) \mapsto \mu_{i \to j}$, gamma $: \nu \mapsto G \in {\rm Sym}(n)$ where $G = (G_{ij})$ is the symmetric matrix such that for $i < j$, $G_{ij} = f(\nu_{i \to j})$ and $G$ has zero coefficients on its diagonal.gammaPartialf $: \nu \mapsto Gp$ where $Gp$ is a np.ndarray of size $(n,n,n)$ and such that for $k \in \{1, \ldots , n\}$, $Gp [k , : , :]$ is a symmetric matrix with $1$ on its diagonal and
$$
Gp [k , : , :] = 
\begin{bmatrix}
1 &   &   & \\
& \ddots & \partial_k f(\nu_{i \to j})  & \\
&  \partial_k f(\nu_{j \to i}) & \ddots & \\
&   &   & 1 \\
\end{bmatrix} 
$$lam $: \mu \mapsto (\lambda_1, \ldots, \lambda_n)$ defined as $\lambda_k = \sum_{j=k}^n \frac{\nu_k}{k}$.def troncNuItoJ(nu,i,j):
    nuij = np.zeros_like(nu)
    nuij[(i-1):(j-1)] = nu[(i-1):(j-1)]
    return nuij
def gamma(nu):
    n = nu.size
    G = np.ones((n,n))
    for i in range(n):
        for j in range(i+1,n):
            G[i,j] = f(troncNuItoJ(nu,i+1,j+1))
            G[j,i] = f(troncNuItoJ(nu,i+1,j+1))
    return G
def gammaPartialf(nu):
    n = nu.size
    Gp = np.ones((n,n,n)) # attention : pour diviser on met 1 et pas 0 sur diag
    for k in range(n):
        for i in range(n):
            for j in range(i+1,n):
                Gp[k,i,j] = partialf(troncNuItoJ(nu,i+1,j+1), k+1)
                Gp[k,j,i] = Gp[k,i,j]
    return Gp
nu = np.array([0.1, 0.2, 0.4, 0.05, 0.15, 0.1])
print('nu = '+str(nu))
print('f(nu) = '+str(f(nu))) 
print('d_3 f(nu) = '+str(partialf(nu, 3))) 
print('nu_{2 to 5} = '+str(troncNuItoJ(nu, 2, 5)))
print('gamma(nu) ='+str(gamma(nu)))
def lam(mu):
    n = mu.size
    lamb = np.flip(np.cumsum(np.flip(mu/np.arange(1,n+1))))
    return lamb
print('lam(nu) = '+str(lam(nu)))
nu = [0.1  0.2  0.4  0.05 0.15 0.1 ]
f(nu) = 0.12374368670764584
d_3 f(nu) = 0.20203050891044214
nu_{2 to 5} = [0.   0.2  0.4  0.05 0.   0.  ]
gamma(nu) =[[1.         0.025      0.0559017  0.11456439 0.11524431 0.121192  ]
 [0.025      1.         0.05       0.1118034  0.1125     0.11858541]
 [0.0559017  0.05       1.         0.1        0.10077822 0.10752907]
 [0.11456439 0.1118034  0.1        1.         0.0125     0.03952847]
 [0.11524431 0.1125     0.10077822 0.0125     1.         0.0375    ]
 [0.121192   0.11858541 0.10752907 0.03952847 0.0375     1.        ]]
lam(nu) = [0.3925     0.2925     0.1925     0.05916667 0.04666667 0.01666667]
We carry on with the implementation of the numerical scheme allowing to compute $(\mu^{(p)}, U^{(p)}) \in \mathring{\Delta}(n) \times O(n)$.
The function geodStartnD takes as imput:
X that contains the initial speed. More precisely, $X$ has size $(n-1) + \frac12 n(n-1)$ and the $n-1$ first entries give $(\mu^\prime)^{(0)}$ while the $\frac12 n(n-1)$ left ones are the upper coefficient of $B^{(0)}$,nu0 of size $n$ that corresponds to $\mu^{(0)}$,U0 of size $(n,n)$ that corresponds to $U^{(0)}$.Thanks to equations $(1)$ to $(4)$, the function geodStartnD computes
tps of discrete times $t_p = p h$ with time step $h$ (default $h = 0.001$)nu of size $(tps.size, n)$ such that nu[p] corresponds to $\mu^{(p)}$,U of size $(tps.size, n, n)$ such that U[p] corresponds to $U^{(p)}$,length of size $tps.size$ such that length[p] is an approximate arclength $\sqrt{g_{\gamma(t_p)}(\gamma^\prime(t_p), \gamma^\prime(t_p))} = \sqrt{|(\mu^\prime)^{(p)}|^2 + \sum_{1\leq i < j \leq n} f(\mu_{i \to j}^{(p)})^2 (b_{ij}^{(p)})^2}$,energy that is an approximate energy of the curve $\frac12 \int_0^L g_{\gamma(t)}(\gamma^\prime(t), \gamma^\prime(t)) \: dt$ (trapezoidal rule),def geodStartnD(X, nu0, U0, h = 0.001):
    n = nu0.size
    #
    # time discretization
    #h = 0.001
    L = 2.
    tps = np.arange(0,L+h,h)
    N = tps.size
    #
    # Declaration of variables
    U = np.zeros((N,n,n))
    nu = np.zeros((N,n))
    lambdas = np.zeros((N,n))
    B = np.zeros((N,n,n))
    nuprime = np.zeros((N,n))
    C = np.zeros((n,n))
    #
    # Building nuprime0 and B0 from X
    nuprime0short = X[0:n-1]; b0 = X[n-1:]
    nuprime0 = np.zeros(n)
    nuprime0[:n-1] = nuprime0short
    nuprime0[n-1] = -nuprime0short.sum()
    B0 = np.zeros((n,n))
    ind = 0
    for i in range(n):
        for j in range(i+1,n):
            B0[i,j] = b0[ind]
            B0[j,i] = - B0[i,j]
            ind += 1
    #
    # t = 0:
    U[0] = U0
    nu[0] = nu0
    lambdas[0] = lam(nu0)
    B[0] = B0
    nuprime[0] =  nuprime0
    G = gamma(nu0)
    C = B0*G**2
    Ctilde = U0 @ C @ np.transpose(U0)
    length = np.zeros(N)
    energy = 0
    #
    # main iteration
    for it in range(N-1):
        # break condition in case nu takes negative values
        if  (nu[it] < 0).sum() > 0:
            print("error: nu < 0")
            break
        else:
            # numercial arglenth and energy computation
            length[it] = np.sqrt(np.linalg.norm(nuprime[it])**2 + np.sum(G**2*B[it]**2))
            energy += 0.25*(np.linalg.norm(nuprime[it])**2 + np.sum(G**2*B[it]**2))*h
            # Eq (2) and (3)
            U[it+1] = np.dot(U[it], linalg.expm(h*B[it]))
            nu[it+1] = nu[it] + h*nuprime[it]
            # Eq (4)
            Gp = gammaPartialf(nu[it])
            M = np.sum(Gp, axis=0)
            for k in range(n):
                Mk = (M - n*Gp[k])*G*B[it]**2
                nuprime[it+1,k] = nuprime[it,k] + h/n*np.sum(Mk)
            # Eq (1) for next iteration
            C = np.transpose(U[it+1]) @ Ctilde @ U[it+1]
            G = gamma(nu[it+1])
            B[it+1] = C/G**2
            #
            lambdas[it+1] = lam(nu[it+1])
            #
            energy += 0.25*(np.linalg.norm(nuprime[it])**2 + np.sum(G**2*B[it]**2))*h
            #
            # break condition if the numerical arclength is too far from constant   
            #BREAK CONDITION IMPLEMENTED IN THE NUMERICAL EXPERIMENT OF THE PAPER
            if it>1 and np.abs(length[it]-length[it-1])/length[it]>0.01:
                break
            #BETTER BREAK CONDITION
            #if it>6 and np.sum(np.abs(length[it-5:it]-length[it-6:it-1]))/h > 10.:
                #break
            
            
    return tps[:it], nu[:it], U[:it], lambdas[:it], nuprime[:it], length[:it], energy, h
The following function traceGeod allows to visualize a path $\gamma = (\mu, \nu)$, for instance obtained via the numerical computation above, in case $n = 3$.
See examples below, corresponding to the numerical experiments displayed in the paper.
def traceGeod(tps,nu,U,lambdas, nuprime,l, save = False, filename = '0'):
    n = nu[0].size
    #
    print('nu0 = '+str(np.round(nu[0],3)))
    print('nu1 = '+str(np.round(nu[-1],3)))
    print('U0 = '+str(np.round(U[0],3)))
    print('U1 = '+str(np.round(U[-1],3)))
    #
    itMax = nu.shape[0]
    print('itMax = '+str(itMax))
    print('temps max = '+str(tps[-1]))
    per = int((itMax-1)/9)
    #
    %matplotlib inline
    plt.figure(1, figsize=(21,7))
    plt.subplot(1,3,1)
    labels1 = []
    for i in range(3):
        labels1 = labels1 + [r'$\mu$'+str(i+1)+r'$ :$'+str(round(nu[0,i],3))+r'$\rightarrow $'+str(round(nu[-1,i],3))]
        plt.plot(tps,nu[:,i])
    plt.plot(tps,np.sum(nu, axis=1))
    labels1 = labels1+[r'$\sum_i \mu_i = 1$']
    plt.legend(labels1)
    plt.title(r'$\mu_i$ en fonction du temps')
    #
    plt.subplot(1,3,2)
    labels2 = []
    for i in range(3):
        labels2 = labels2 + [r'$\lambda$'+str(i+1)+r'$ :$'+str(round(lambdas[0,i],3))+r'$\rightarrow $'+str(round(lambdas[-1,i],3))]
        plt.plot(tps,lambdas[:,i])
    plt.legend(labels2)
    plt.title(r'$\lambda_i$ en fonction du temps')
    # curve in the simplex of vertices (-1,0), (1,0) et (0,np.sqrt(3))
    plt.subplot(1,3,3)
    labels3 = []
    for i in range(3):
        labels3 = labels3 + [r'$\mu^\prime$'+str(i+1)+r'$ :$'+str(round(nuprime[0,i],3))+r'$\rightarrow $'+str(round(nuprime[-1,i],3))]
        plt.plot(tps,nuprime[:,i])
    plt.plot(tps,np.sum(nuprime, axis=1))
    labels3 = labels3+[r'$\sum_i \mu_i^\prime = 0$']
    plt.legend(labels3)
    plt.title(r'$\mu_i^\prime$ en fonction du temps')
    #
    fig = plt.figure(2,figsize=(21,7),frameon=False)
    plt.title(r'ellipsoid whose axes are the columns of $U = (U_1, U_2, U_3)$ associated with semi-axes length $(\sqrt{\lambda_1}, \sqrt{\lambda_2}, \sqrt{\lambda_3} )$ plotted at different times')
    plt.axis('off')
    ax1, ax2, ax3 = fig.subplots(nrows=1, ncols=3)
    for i in range(10):
        x,y,z = ellipsoid(n*lam(nu[per*i]),U[per*i])
        ax1.plot(i+x.reshape(100*100),i+y.reshape(100*100))
        ax1.set_xlabel('frame (x,y)')
        ax2.plot(i+y.reshape(100*100),i+z.reshape(100*100))
        ax2.set_xlabel('frame (y,z)')
        ax3.plot(i+x.reshape(100*100),i+z.reshape(100*100))
        ax3.set_xlabel('frame (x,z)')
    plt.show()
    #
    theta = np.zeros((itMax,2))
    fig = plt.figure(3, figsize=(21,7))
    plt.subplot(1,3,1)
    plt.title(r'$i^{th}$ principal angle $\theta_i$ between $i$--dimensional subspaces ${\rm span} (U_1, \ldots, U_i)$ at times $0$ and $t$')
    for i in range(1,3):
        for k in range(itMax):
            #A = np.dot(U[0,:,:i].T, U[k,:,:i])
            s = np.linalg.svd(np.dot(U[0,:,:i].T, U[k,:,:i]))[1]
            theta[k,i-1] = np.arccos(min(1.,s[i-1]))
    plt.plot(tps, theta)
    #plt.plot(tps, np.arctan2(U[:,1,0],U[:,0,0]))
    plt.legend([r'$\theta_1$', r'$\theta_2$'])
    #
    plt.subplot(1,3,2)
    plt.plot(-nu[:,0] + nu[:,1], np.sqrt(3)*nu[:,2], label = r'$t \mapsto \mu(t) $')
    plt.plot([-1,1,0,-1],[0,0,np.sqrt(3),0])
    plt.title(r'Image of the curve of $t \mapsto \mu(t)$ in the simplex')
    plt.legend()
    plt.ylim(-0.2,2)
    #
    plt.subplot(1,3,3)
    plt.plot(tps[1:], l[1:])
    plt.ylim(0,3)
    plt.title(r'numerical arclength $t \mapsto \sqrt{g_{\gamma(t)}(\gamma^\prime(t), \gamma^\prime(t))}$')
    plt.show()
    #
    if save:
        plt.rcParams.update({'font.size': 20, 'lines.linewidth' : 4})
        plt.figure(10, figsize=(7,7))
        for i in range(3):
            plt.plot(tps,nu[:,i])
        plt.plot(tps,np.sum(nu, axis=1))
        plt.legend(labels1, fontsize=18)
        plt.savefig('flagfoldsFigures/nu'+filename+'.pdf')
        plt.figure(11, figsize=(7,7))
        for i in range(3):
            plt.plot(tps,lambdas[:,i])
        plt.legend(labels2, fontsize=18)
        plt.savefig('flagfoldsFigures/lambda'+filename+'.pdf')
        plt.figure(12, figsize=(7,7))
        for i in range(3):
            plt.plot(tps,nuprime[:,i])
        plt.plot(tps,np.sum(nuprime, axis=1))
        plt.legend(labels3, fontsize=18, loc = 'best')
        plt.savefig('flagfoldsFigures/nuprime'+filename+'.pdf')
        plt.figure(13, figsize=(7,7))
        for i in range(10):
            x,y,z = ellipsoid(n*lam(nu[per*i]),U[per*i])
            plt.plot(i+x.reshape(100*100),i+y.reshape(100*100))
            #plt.xlabel('frame (x,y)')
        plt.savefig('flagfoldsFigures/ellipsoidxy'+filename+'.pdf')
        plt.figure(14, figsize=(7,7))
        for i in range(10):
            x,y,z = ellipsoid(n*lam(nu[per*i]),U[per*i])
            plt.plot(i+y.reshape(100*100),i+z.reshape(100*100))
            #plt.xlabel('frame (y,z)')
        plt.savefig('flagfoldsFigures/ellipsoidyz'+filename+'.pdf')
        plt.figure(15, figsize=(7,7))
        for i in range(10):
            x,y,z = ellipsoid(n*lam(nu[per*i]),U[per*i])
            plt.plot(i+x.reshape(100*100),i+z.reshape(100*100))
            #plt.xlabel('frame (x,z)')
        plt.savefig('flagfoldsFigures/ellipsoidxz'+filename+'.pdf')
        plt.figure(16, figsize=(7,7))
        for i in range(1,3):
            for k in range(itMax):
            #A = np.dot(U[0,:,:i].T, U[k,:,:i])
                s = np.linalg.svd(np.dot(U[0,:,:i].T, U[k,:,:i]))[1]
                theta[k,i-1] = np.arccos(min(1.,s[i-1]))
        plt.plot(tps, theta)
        plt.legend([r'$\theta_1$', r'$\theta_2$'], fontsize=18)
        plt.savefig('flagfoldsFigures/theta'+filename+'.pdf')
        plt.figure(17, figsize=(7,7))
        plt.plot(-nu[:,0] + nu[:,1], np.sqrt(3)*nu[:,2], label = r'$t \mapsto \nu(t) $')
        plt.plot([-1,1,0,-1],[0,0,np.sqrt(3),0])
        plt.ylim(-0.2,2)
        plt.savefig('flagfoldsFigures/simplex'+filename+'.pdf')
        plt.figure(18, figsize=(7,7))
        plt.plot(tps[1:], l[1:])
        plt.ylim(0,3)
        plt.savefig('flagfoldsFigures/arclength'+filename+'.pdf')
        #
        #plt.rcParams.update({'font.size': 10})
        plt.rcdefaults()
    #
    #    
    %matplotlib notebook
    plt.figure(4, figsize=(9,9))
    ax = plt.axes(projection='3d')
    ax.set_axis_off()
    boxSize = 10
    ax.set_xlim(-1, boxSize) ; ax.set_ylim(-1, boxSize) ; ax.set_zlim(-1, boxSize)
    ax.set_box_aspect((1,1,1))
    #
    for i in range(10):
        #print('temps '+str(round(tps[per*i],3))+' : U = '+str(np.round(U[per*i],3))+' and  nu = '+str(np.round(nu[per*i],3)))
        x,y,z = ellipsoid(n*lam(nu[per*i]),U[per*i])
        ax.plot_surface(i+x, i+y, i+z,  rstride=4, cstride=4)
##Example 1: line to plane
X = np.array([-1,  1, 0.05, 0.5, 0.]); nu0 = np.array([0.98, 0.01, 0.01]); U0 = np.eye(3)
tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
traceGeod(tps,nu,U, lambdas, nuprime, l, False,'LineToPlane')
nu0 = [0.98 0.01 0.01] nu1 = [0.028 0.95 0.023] U0 = [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]] U1 = [[ 0.039 0.738 0.674] [-0.997 0.072 -0.021] [-0.064 -0.671 0.739]] itMax = 941 temps max = 0.9400000000000001
##Example 2: complete flag to line
X = np.array([1,  -0.5, 0.5, 0., 0.]); nu0 = np.array([1/3, 1/3, 1/3]); U0 = np.eye(3)
tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
traceGeod(tps,nu,U, lambdas, nuprime, l, False,'CompleteFlagToLine')
error: nu < 0 nu0 = [0.333 0.333 0.333] nu1 = [1. 0. 0.] U0 = [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]] U1 = [[ 0.994 0.111 0. ] [-0.111 0.994 0. ] [ 0. 0. 1. ]] itMax = 668 temps max = 0.667
##Example 3: plane-line to line-R^3
X = np.array([0.15,  -0.5, 0.5, 0.1, 0.]); nu0 = np.array([0.499, 0.499, 0.002]); U0 = np.eye(3)
tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
traceGeod(tps,nu,U, lambdas, nuprime, l, False,'PlaneLineToR3')
nu0 = [0.499 0.499 0.002] nu1 = [0.643 0.008 0.349] U0 = [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]] U1 = [[ 0.921 0.094 0.378] [-0.372 0.5 0.782] [-0.115 -0.861 0.496]] itMax = 987 temps max = 0.986
##Example 4: line to R^3
X = np.array([-1,  0, 0., 0., 0.]); nu0 = np.array([0.98, 0.01, 0.01]); U0 = np.eye(3)
tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
traceGeod(tps,nu,U, lambdas, nuprime, l, False,'LineToR3')
error: nu < 0 nu0 = [0.98 0.01 0.01] nu1 = [0.001 0.01 0.989] U0 = [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]] U1 = [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]] itMax = 980 temps max = 0.979
##Example 5: plane to R^3
#X = np.array([0.,  -1., 0., 0., 0.]); nu0 = np.array([0.01, 0.98, 0.01]); U0 = np.eye(3)
#tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
#traceGeod(tps,nu,U, lambdas, nuprime, l, False,'PlaneToR3')
##Example 6: plane to plane
#X = np.array([-0.02,  0.04, 0., 0.75, 0.]); nu0 = np.array([0.01, 0.98, 0.01]); U0 = np.eye(3)
#tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
#traceGeod(tps,nu,U, lambdas, nuprime, l, False,'PlaneToPlane')
##Example 7: line to line
#X = np.array([0.04,  -0.02, 0., 0.75, 0.]); nu0 = np.array([0.98, 0.01, 0.01]); U0 = np.eye(3)
#tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
#traceGeod(tps,nu,U, lambdas, nuprime, l, False,'LineToLine')
##Example 8 : plane-line to plane
#X = np.array([-0.5,  0.5, 0., 0.75, 0.]); nu0 = np.array([0.499, 0.499, 0.002]); U0 = np.eye(3)
#tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(X, nu0, U0)
#traceGeod(tps,nu,U, lambdas, nuprime, l, False,'PLaneLineToPlane')
We compare the numerical geodesic obtained in Example 1 with the euclidean geodesic joining the same endpoints.
def lamToNu(lbd):
    n = lbd.size
    mu = np.zeros(n)
    for k in range(n-1):
        mu[k] = (k+1)*(lbd[k] - lbd[k+1])
    mu[-1] = n*lbd[-1]
    return mu
def euclGeod(tps,nu0,nu1,U0,U1):
    n = nu0.size
    M0 = U0 @ np.diag(lam(nu0)) @ np.transpose(U0)
    M1 = U1 @ np.diag(lam(nu1)) @ np.transpose(U1)
    N = tps.size
    U = np.zeros((N,n,n))
    t = 0
    lambdas = np.zeros((N,n))
    nu = np.zeros((N,n))
    nuprime = np.zeros((N-1,n))
    length = np.zeros((N,n))
    for i in range(N):
        t = tps[i]/tps[-1]
        #M = (1-t)*M0 + t*M1
        lambdas[i], U[i] = np.linalg.eigh((1-t)*M0 + t*M1)
        lambdas[i] = np.flip(lambdas[i])
        U[i] = np.flip(U[i], axis = 1)
        nu[i] = lamToNu(lambdas[i])
        if i>0:
            nuprime[i-1] = (nu[i] - nu[i-1])/(tps[i]-tps[i-1])
            G = gamma(nu[i])
            B = np.transpose(U[i]) @ (U[i]-U[i-1])/(tps[i]-tps[i-1])
            length[i] = np.sqrt(np.linalg.norm(nuprime[i-1])**2 + np.sum(G**2*B**2))
    return tps, nu, U, lambdas, length
tps, nu, U, lambdas, nuprime, l, e, h = geodStartnD(np.array([-1,  1, 0.05, 0.5, 0.]), np.array([0.98, 0.01, 0.01]),U0)
tps, nu, U, lambdas, l = euclGeod(tps, nu[0], nu[-1], U[0], U[-1])
traceGeod(tps,nu,U, lambdas, nuprime, l, False,'LineToPlaneEucl')
        
 
        
nu0 = [0.98 0.01 0.01] nu1 = [0.028 0.95 0.023] U0 = [[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]] U1 = [[-0.039 0.738 -0.674] [ 0.997 0.072 0.021] [ 0.064 -0.671 -0.739]] itMax = 941 temps max = 0.9400000000000001