"""
Horizontal Functional Principal Nested Spheres Analysis using SRSF
moduleauthor:: J. Derek Tucker <jdtuck@sandia.gov>
"""
import numpy as np
import fdasrsf as fs
import fdasrsf.pns as pns
import fdasrsf.utility_functions as uf
from scipy.integrate import cumulative_trapezoid
import matplotlib.pyplot as plt
import fdasrsf.plot_style as plot
[docs]
class fdahpns:
"""
This class provides horizontal fPNS using the
SRVF framework
Usage: obj = fdapns(warp_data)
:param warp_data: fdawarp class with alignment data
:param gam_pca: warping functions principal directions
:param psi_pca: srvf principal directions
:param latent: latent values
:param U: eigenvectors
:param coef: coefficients
:param vec: shooting vectors
:param mu: Karcher Mean
:param tau: principal directions
Author : J. D. Tucker (JDT) <jdtuck AT sandia.gov>
Date : 03-Apr-2025
"""
def __init__(self, fdawarp):
"""
Construct an instance of the fdahpns class
:param fdawarp: fdawarp class
"""
if fdawarp.fn.size == 0:
raise Exception("Please align fdawarp class using srsf_align!")
self.warp_data = fdawarp
[docs]
def calc_pns(self, var_exp=0.99, stds=np.arange(-1, 2)):
"""
This function calculates horizontal functional principal nested
spheres on aligned data
:param var_exp: compute no based on value percent variance explained
(example: 0.95)
:param stds: number of standard deviations along geodesic to compute
(default = -1,0,1)
:rtype: fdapns object of numpy ndarray
:return gam_pca: srsf principal directions
:return psi_pca: functional principal directions
:return latent: latent values
:return coef: coefficients
:return U: eigenvectors
"""
gam = self.warp_data.gam
d, n = gam.shape
t = np.linspace(0, 1, d)
mu, gam_mu, psi, vec = uf.SqrtMean(gam)
radius = np.mean(np.sqrt((psi**2).sum(axis=0)))
pnsdat = psi / np.tile(np.sqrt((psi**2).sum(axis=0)), (d, 1))
resmat, PNS = pns.PNSmainHDLSS(pnsdat)
# Proportion of variance explained
varPNS = np.sum(np.abs(resmat) ** 2, axis=1) / n
cumvarPNS = np.cumsum(varPNS)
propcumPNS = cumvarPNS / cumvarPNS[-1]
propPNS = varPNS / cumvarPNS[-1] * 100
# Projection of PCs
no = int(np.argwhere(propcumPNS <= var_exp)[-1])+1
if (no == 1):
no += 1
projPsi = np.zeros((d, stds.shape[0], no))
projGam = np.zeros((d, stds.shape[0], no))
for PCnum in range(no):
std = resmat[PCnum, :].std()
mean = resmat[PCnum, :].mean()
dirtmp = stds * std + mean
restmp = np.zeros((resmat.shape[0], stds.shape[0]))
restmp[PCnum, :] = dirtmp
PCvec = fs.pns.PNSe2s(restmp, PNS)
projPsi[:, :, PCnum] = PCvec * radius
gamt = cumulative_trapezoid(projPsi[:, :, PCnum] ** 2, t, axis=0,
initial=0)
for j in range(stds.shape[0]):
gamt[:, j] = (gamt[:, j] - gamt[:, j].min()) / (
gamt[:, j].max() - gamt[:, j].min()
)
projGam[:, :, PCnum] = gamt
self.gam_pns = projGam
self.psi_pns = projPsi
self.cumvar = propcumPNS
self.no = no
self.psi = psi
self.PNS = PNS
self.coef = resmat
self.radius = radius
return
[docs]
def project(self, f):
"""
project new data onto fPNS basis
Usage: obj.project(f)
:param f: numpy array (MxN) of N functions on M time
"""
q1 = fs.f_to_srsf(f, self.warp_data.time)
M = self.warp_data.time.shape[0]
n = q1.shape[1]
mq = self.warp_data.mqn
gam = np.zeros((M, n))
for ii in range(0, n):
gam[:, ii] = fs.optimum_reparam(mq, self.warp_data.time, q1[:, ii])
psi = np.zeros((M, n))
time = np.linspace(0, 1, M)
binsize = np.mean(np.diff(time))
for i in range(0, n):
psi[:, i] = np.sqrt(np.gradient(gam[:, i], binsize))
pnsdat = psi / np.tile(np.sqrt((psi**2).sum(axis=0)), (n, 1))
resmat = pns.PNSs2e(pnsdat, self.PNS)
self.new_coef = resmat
return
[docs]
def plot(self):
"""
plot plot elastic horizontal fPNS results
Usage: obj.plot()
"""
no = self.no
TT = self.warp_data.time.shape[0]
num_plot = int(np.ceil(no / 3))
colors = [
"#66C2A5",
"#FC8D62",
"#8DA0CB",
"#E78AC3",
"#A6D854",
"#FFD92F",
"#E5C494",
"#B3B3B3",
]
k = 0
for ii in range(0, num_plot):
if k > (no - 1):
break
fig, ax = plt.subplots(1, 3)
for k1 in range(0, 3):
k = k1 + (ii) * 3
axt = ax[k1]
if k > (no - 1):
break
axt.plot(np.linspace(0, 1, TT), np.squeeze(self.gam_pns[:, :, k]))
plt.style.use("seaborn-v0_8-colorblind")
axt.set_title("PD %d" % (k + 1))
axt.set_aspect("equal")
fig.set_tight_layout(True)
cumm_coef = 100 * self.cumvar
idx = np.arange(0, self.cumvar.shape[0]) + 1
plot.f_plot(idx, cumm_coef, "Coefficient Cumulative Percentage")
plt.ylabel("Percentage")
plt.xlabel("Index")
plt.show()
return
def project_pns_gam(resmat, PNS, radius, time):
n = resmat.shape[1]
d = time.shape[0]
udir = np.eye(resmat.shape[0])
PCvec = pns.PNSe2s(udir@resmat, PNS) * radius
gam_hat = np.zeros((d, n))
for i in range(n):
gamt = cumulative_trapezoid(PCvec[:, i]**2, time, initial=0)
gam_hat[:, i] = (gamt - gamt.min()) / (gamt.max() - gamt.min())
return gam_hat