# barycentric.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

import numpy as np

def f(x):
    return 1/(1 + 25*x**2)

def generatedata(n,f,nodes="cheb"):
    if nodes=="cheb":
        dataxs = -np.cos(np.linspace(0,np.pi,n))
    else:
        dataxs = np.linspace(-1,1,n)
    datays = f(dataxs)
    return dataxs, datays

def weights(dataxs):
    n = dataxs.size
    ws = np.ones(n)
    for k in range(n):
        for j in range(n):
            if j == k:
                continue
            ws[k] *= (dataxs[k]-dataxs[j])
    return 1/ws

def bary(dataxs,datays,ws,x):
    k = np.where(x == dataxs)[0]
    if k.size == 0:
        nume = np.sum(ws*datays/(x-dataxs))
        denom = np.sum(ws/(x-dataxs))
        val = nume/denom
    else:
        val = datays[k[0]]
    return val

if __name__ == '__main__':
    dataxs, datays = generatedata(15, f)
    ws = weights(dataxs)
    x = 0.3; pofx = bary(dataxs, datays, ws, x)
    print(x, pofx, f(x))
# splines.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from barycentric import f, generatedata
from gauelim_pivot import gauelim_pivot
import numpy as np

def computecs(dataxs,datays):
    n = dataxs.size
    A = np.zeros((n-2,n-2))
    np.fill_diagonal(A, 2*(dataxs[2:]-dataxs[:-2]))
    np.fill_diagonal(A[1:,:], dataxs[2:-1]-dataxs[1:-2])
    np.fill_diagonal(A[:,1:], dataxs[2:-1]-dataxs[1:-2])

    b1 = (datays[2:]-datays[1:-1])/(dataxs[2:]-dataxs[1:-1])
    b2 = (datays[1:-1]-datays[:-2])/(dataxs[1:-1]-dataxs[:-2])
    bs = 6*(b1 - b2)

    cs = np.zeros(n)
    cs[1:-1] = gauelim_pivot(A, bs)
    return cs

def splineinterp(dataxs,datays,cs,x):
    k = np.argmax(dataxs>x)
    xk = dataxs[k]; xk1 = dataxs[k-1]
    yk = datays[k]; yk1 = datays[k-1]
    ck = cs[k]; ck1 = cs[k-1]

    val = yk1*(xk-x)/(xk-xk1) + yk*(x-xk1)/(xk-xk1)
    val -= ck1*((xk-x)*(xk-xk1) - (xk-x)**3/(xk-xk1))/6
    val -= ck*((x-xk1)*(xk-xk1) - (x-xk1)**3/(xk-xk1))/6
    return val

if __name__ == '__main__':
    dataxs, datays = generatedata(15, f, "equi")
    cs = computecs(dataxs, datays)
    x = 0.95; pofx = splineinterp(dataxs, datays, cs, x) 
    print(x, pofx, f(x))
# triginterp.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from math import pi
import numpy as np

def f(x):
    return np.exp(np.sin(2*x))

def generatedata(n,f):
    dataxs = 2*pi*np.arange(n)/n
    datays = f(dataxs)
    return dataxs, datays

def computeparams(dataxs,datays):
    n = dataxs.size
    m = n//2
    aparams = np.zeros(m+1)
    bparams = np.zeros(m-1)
    
    for k in range(m+1):
        aparams[k] = datays@np.cos(k*dataxs)/m
    for k in range(1,m):
        bparams[k-1] = datays@np.sin(k*dataxs)/m
    return aparams, bparams

def triginterp(aparams,bparams,x):
    n = aparams.size + bparams.size
    m = n//2
    val = 0.5*(aparams[0] + aparams[-1]*np.cos(m*x))
    for k in range(1,m):
        val += aparams[k]*np.cos(k*x)
        val += bparams[k-1]*np.sin(k*x)
    return val

if __name__ == '__main__':
    dataxs, datays = generatedata(8, f)
    aparams, bparams = computeparams(dataxs, datays)
    x = 0.3; pofx = triginterp(aparams, bparams, x)
    print(x,pofx)
# fft.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from triginterp import f, generatedata
from math import pi
import numpy as np

def fft(ys):
    n = ys.size
    m = n//2
    if n==1:
        ytils = ys
    else:
        evens = fft(ys[::2])
        odds = fft(ys[1::2])
        coeffs = np.exp(-2*pi*np.arange(m)*1j/n)
        first = evens + coeffs*odds
        second = evens - coeffs*odds
        ytils = np.concatenate((first, second))
    return ytils

def fftinterp(ytils,x):
    n = ytils.size
    m = n//2
    val = ytils[:m]@np.exp(np.arange(m)*x*1j)
    val += ytils[m]*np.cos(m*x)
    val += ytils[m+1:]@np.exp(np.arange(-m+1,0)*x*1j)
    return val/n

if __name__ == '__main__':
    n = 8
    dataxs, datays = generatedata(n, f)
    ytils = fft(datays)
    x = 0.3; pofx = fftinterp(ytils, x)
    print(x,pofx.real)
# linefit.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

import numpy as np

def helpers(dataxs,datays,datasigs):
    S = np.sum(1/datasigs**2)
    Sx = np.sum(dataxs/datasigs**2)
    Sy = np.sum(datays/datasigs**2)
    Sxx = np.sum(dataxs**2/datasigs**2)
    Sxy = np.sum(dataxs*datays/datasigs**2)
    Del = S*Sxx - Sx**2
    return S, Sx, Sy, Sxx, Sxy, Del

def computecs(dataxs,datays,datasigs):
    S,Sx,Sy,Sxx,Sxy,Del = helpers(dataxs,datays,datasigs)
    cs = np.zeros(2); dcs = np.zeros(2)
    cs[0] = (Sxx*Sy - Sx*Sxy)/Del
    cs[1] = (S*Sxy - Sx*Sy)/Del
    dcs[0] = np.sqrt(Sxx/Del)
    dcs[1] = np.sqrt(S/Del)
    return cs, dcs

def computechisq(dataxs,datays,datasigs,cs):
    chisq = np.sum((datays-cs[0]-cs[1]*dataxs)**2/datasigs**2)
    return chisq

dataxs = np.linspace(0,1,6)
datays = np.array([3.085, 3.123, 3.224, 3.360, 3.438, 3.569])
datasigs = np.array([0.048, 0.053, 0.02, 0.005, 0.023, 0.07])

cs, dcs = computecs(dataxs, datays, datasigs)
print(cs); print(dcs)
chisq = computechisq(dataxs, datays, datasigs, cs)
print(chisq/(dataxs.size - cs.size))
# newnormal.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from gauelim_pivot import gauelim_pivot
import numpy as np

def generatedata(N,a=0.,b=9,sts=(2,5,0.5,1)):
    sa, sb, sc, sd = sts
    np.random.seed(7921)
    data = np.zeros((3,N))
    data[0,:] = np.linspace(a,b,N)
    data[1,:] = sa + sb*np.sin(data[0,:])
    data[2,:] = sc + sd*np.random.random(N)
    data[1,:] += np.random.normal(0,data[2,:])
    return data

def phi(n,k,x):
    if n==5:
        val = x**k
    elif n==2:
        val = 1. if k==0 else np.sin(x)
    return val

def normalfit(data,phi,n):
    N = data.shape[1]
    A = np.zeros((N,n))
    for k in range(n):
        A[:,k] = phi(n,k,data[0,:])/data[2,:]
    bs = data[1,:]/data[2,:]

    cs = gauelim_pivot(A.T@A, A.T@bs)
    chisq = np.sum((bs - A@cs)**2)
    return cs, chisq

if __name__ == '__main__':
    data = generatedata(8)
    for n in (5, 2):
        cs, chisq = normalfit(data, phi, n)
        print(cs)
        print(chisq/(data.shape[1]-cs.size))
        print("")
# bayes.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from gauelim_pivot import gauelim_pivot
from newnormal import generatedata, phi
import numpy as np

def inv(A):
    n = A.shape[0]
    invA = np.zeros((n,n))
    for i,bs in enumerate(np.identity(n).T):
        invA[:,i] = gauelim_pivot(A,bs)
    return invA

def bayes(data, batches, primus, priS):
    n = primus.size
    i = 0
    for N in batches:
        A = np.zeros((N,n))
        for k in range(n):
            A[:,k] = phi(n,k,data[0,i:i+N])/data[2,i:i+N]
        bs = data[1,i:i+N]/data[2,i:i+N]

        priSinv = inv(priS)
        postS = inv(A.T@A + priSinv)
        postmus = postS@(A.T@bs + priSinv@primus)
        
        primus, priS = postmus, postS
        i += N
        print(n, i, postmus, postS)
    return postmus, postS
    
if __name__ == '__main__':
    batches = 1, 4, 3
    data = generatedata(np.sum(batches))
    for n in (5, 2):
        primus = np.zeros(n)
        priS = np.zeros((n,n))
        np.fill_diagonal(priS, np.linspace(10,2,n))
        postmus, postS = bayes(data, batches, primus, priS)
        print(postmus)
# neural.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from jacobi import termcrit
from newnormal import generatedata
from numpy import exp
import numpy as np

def soft(der, x):
    return np.log(1+exp(x)) if der==0 else exp(x)/(1+exp(x))

def getzspofx(W, x, act):
    zs = act(0, W[1,:]*x + W[2,:])
    pofx = act(0, W[0,:]@zs + W[3,0])
    return zs, pofx

def backprop(W, x, y, act):
    zs, pofx = getzspofx(W, x, act)
    ders = np.zeros((4,zs.size))
    ders[3,0] = 2*(pofx - y)*act(1, W[0,:]@zs + W[3,0])
    ders[0,:] = ders[3,0]*zs
    ders[1,:] = ders[3,0]*W[0,:]*x*act(1,W[1,:]*x+W[2,:])
    ders[2,:] = ders[3,0]*W[0,:]*act(1,W[1,:]*x+W[2,:])
    return ders

def sgd(W, x, y, act=soft, g=0.006, kmax=300, tol=1.e-4):
    for k in range(1,kmax):
        ders = backprop(W, x, y, act)
        Wn = np.array([W[i,:]-g*ders[i,:] for i in range(4)])
        _, pofxnew = getzspofx(Wn, x, act)
        conds = [termcrit(W[i,:],Wn[i,:]) for i in range(3)]
        if np.all([val<tol for val in conds[:3]]):
            break
        W = np.copy(Wn)
    return Wn, (pofxnew - y)**2

if __name__ == '__main__':
    sts = (1.5,1.2,0.15,0.08)
    data = generatedata(400,0.2,4.2,sts)
    np.random.seed(5179)
    W = np.random.uniform(-1,1,(4,10))
    W, s = sgd(W, data[0,155], data[1,155]); print(s)
# newblack.py
# Author: Alex Gezerlis
# Numerical Methods in Physics with Python (2nd ed., CUP, 2023)

from gauelim_pivot import gauelim_pivot
from jacobi import termcrit
import numpy as np

def generatedata():
    data = np.zeros((3,13))
    data[0,:] = np.array([373.1, 492.5, 733, 755, 799, 820,
            877, 1106, 1125, 1403, 1492, 1522, 1561])
    data[1,:] = np.array([156., 638, 3320, 3810, 4440, 5150,
            6910, 16400, 17700, 44700, 57400, 60600, 67800])
    data[2,:] = np.ones(data.shape[1])
    return data

def model(cs,x):
    return cs[0] + cs[1]*x**cs[2]

def getKrs(data, cs):
    K = np.zeros((data.shape[1], cs.size))
    K[:,0] = 1/data[2,:]
    K[:,1] = data[0,:]**cs[2]/data[2,:]
    K[:,2] = cs[1]*data[0,:]**cs[2]*np.log(data[0,:])/data[2,:]
    rs = (data[1,:] - model(cs, data[0,:]))/data[2,:]
    return K, rs

def gaussnewton(data, colds, kmax=50, tol=1.e-8):
    for k in range(1,kmax):
        K, rs = getKrs(data, colds)
        cnews = colds + gauelim_pivot(K.T@K, K.T@rs)
        err = termcrit(colds,cnews)
        print(k,err)
        if err < tol:
            break
        colds = np.copy(cnews)
    return cnews

if __name__ == '__main__':
    data = generatedata()
    colds = np.array([-700, 1.26e-8, 4.5])
    cs = gaussnewton(data,colds); print(cs)