Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mipals committed Oct 17, 2023
1 parent deec28c commit 9ab052f
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 1 deletion.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

.DS_Store
dist/egrssmatrices-0.0.1-py3-none-any.whl
dist/egrssmatrices-0.0.1.tar.gz
egrssmatrices.egg-info/SOURCES.txt
egrssmatrices.egg-info/PKG-INFO
egrssmatrices.egg-info/top_level.txt
egrssmatrices/__pycache__
egrssmatrices.egg-info/dependency_links.txt
egrssmatrices/ideas.py
tests/__pycache__
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,19 @@
# egrssmatrices


## Description
A package for efficiently computing with symmetric extended generator representable semiseparable matrices (EGRSS Matrices) and a variant with added diagonal terms. In short this means matrices of the form

$$
K = \text{\textbf{tril}}(UV^\top) + \text{\textbf{triu}}\left(VU^\top,1\right), \quad U,V\in\mathbb{R}^{p\times n}
$$

$$
K = \text{\textbf{tril}}(UV^\top) + \text{\textbf{triu}}\left(VU^\top,1\right) + \text{\textbf{diag}}(d), \quad U,V\in\mathbb{R}^{p\times n},\ d\in\mathbb{R}^n
$$

All implemented algorithms (multiplication, Cholesky factorization, forward/backward substitution as well as various traces and determinants) scales with $O(p^kn)$. Since $p \ll n$ this result in very scalable computations.

A more in-depth descriptions of the algorithms can be found in [1] or [here](https://github.com/mipals/SmoothingSplines.jl/blob/master/mt_mikkel_paltorp.pdf).

## References
[1] M. S. Andersen and T. Chen, “Smoothing Splines and Rank Structured Matrices: Revisiting the Spline Kernel,” SIAM Journal on Matrix Analysis and Applications, 2020.
Empty file added egrssmatrices/__init__.py
Empty file.
158 changes: 158 additions & 0 deletions egrssmatrices/egrssmatrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import numpy as np

class egrssmatrix:
def __init__(self,Ut,Vt,d=None):
self.Ut = Ut
self.Vt = Vt
self.p, self.n = Ut.shape
self.d = d
self.Wt = None
self.c = None
self.Yt = None
self.Zt = None
self.shape = (self.n, self.n)
self.dtype = Ut.dtype

def __repr__(self):
return f"egrssmatrix(p={self.p},n={self.n})"

def __add__(self,other):
return egrssmatrix(np.hstack((self.Ut,other.Ut)), np.hstack((self.Ut,other.Ut)))

def __matmul__(self,other):
assert self.n == other.shape[0], f"dimension error. Expected length to be {self.n} got {other.shape[0]}"
ubar = self.Ut @ other
vbar = np.zeros(ubar.shape)
yk = np.zeros(other.shape)
for k in range(self.n):
vbar = vbar + self.Vt[:,k]*other[k]
ubar = ubar - self.Ut[:,k]*other[k]
yk[k] = np.dot(self.Ut[:,k],vbar) + np.dot(self.Vt[:,k],ubar)
if self.d is not None:
yk = yk + self.d*other
return yk

def __getitem__(self, key):
if key[0] > key[1]:
return np.dot(self.Ut[:,key[0]], self.Vt[:,key[1]])
elif key[0] == key[1]:
if self.d is not None:
return np.dot(self.Ut[:,key[0]], self.Vt[:,key[1]]) + self.d[key[0]]
else:
return np.dot(self.Ut[:,key[0]], self.Vt[:,key[1]])
return np.dot(self.Vt[:,key[0]], self.Ut[:,key[1]])

def __setitem__(self,key,value):
raise IndexError('You can not set index value of an egrssmatrix')

def matvec(self,other):
return self@other

def full(self):
K = np.tril(self.Ut.T@self.Vt,-1)
K = K + K.T + np.diag(np.sum(self.Ut*self.Vt,axis=0))
if self.d is not None:
if np.isscalar(self.d):
K = K + np.diag(self.d*np.ones(self.n))
else:
K = K + np.diag(self.d)
return K

def cholesky(self):
wbar = np.zeros((self.p,self.p))
Wt = self.Vt.copy()
if self.d is None:
for k in range(self.n):
Wt[:,k] = Wt[:,k] - wbar@self.Ut[:,k]
Wt[:,k] = Wt[:,k]/np.sqrt(np.dot(self.Ut[:,k],Wt[:,k]))
wbar = wbar + np.outer(Wt[:,k],Wt[:,k])
self.Wt = Wt
self.c = np.sum(self.Ut*self.Wt,axis=0)
else:
if np.isscalar(self.d):
c = self.d*np.ones(self.n)
else:
c = self.d.copy()

for k in range(self.n):
Wt[:,k] = Wt[:,k] - wbar@self.Ut[:,k]
c[k] = np.sqrt(np.dot(self.Ut[:,k],Wt[:,k]) + c[k])
Wt[:,k] = Wt[:,k]/c[k]
wbar = wbar + np.outer(Wt[:,k],Wt[:,k])
self.Wt = Wt
self.c = c

def solve(self,other):
return self.backward(self.forward(other))

def forward(self,other):
if self.Wt is None:
self.cholesky()
wbar = np.zeros(self.p)
x = np.zeros(self.n)
for k in range(self.n):
x[k] = (other[k] - np.dot(self.Ut[:,k],wbar))/self.c[k]
wbar = wbar + self.Wt[:,k]*x[k]
return x

def backward(self,other):
if self.Wt is None:
self.cholesky()
ubar = np.zeros(self.p)
x = np.zeros(self.n)
for k in reversed(range(self.n)):
x[k] = (other[k] - np.dot(self.Wt[:,k],ubar))/self.c[k]
ubar = ubar + self.Ut[:,k]*x[k]
return x

def cholesky_inverse(self):
if self.Yt is not None and self.Zt is not None:
return
if self.Wt is None:
self.cholesky()
Yt = self.Ut.copy()
Zt = self.Wt.copy()

for k in range(self.p):
Yt[k,:] = self.forward(self.Ut[k,:])
Zt[k,:] = self.backward(self.Wt[k,:])

Zt = np.linalg.solve(Zt@self.Ut.T - np.eye(self.p), Zt)

self.Yt = Yt
self.Zt = Zt

def logdet(self):
if self.c is None:
self.cholesky()
return 2*np.sum(np.log(self.c))

def trace_inverse(self):
if self.Yt is None and self.Zt is None:
self.cholesky_inverse()

Ybar = np.zeros((self.p,self.p))
b = 0
for k in reversed(range(self.n)):
b = b + self.c[k]**(-2) + self.Zt[:,k]@(Ybar@self.Zt[:,k])
Ybar = Ybar + np.outer(self.Yt[:,k],self.Yt[:,k])

return b

def trace_inverse_product(self):
if self.Yt is None and self.Zt is None:
self.cholesky_inverse()
b = 0
P = np.zeros((self.p,self.p))
R = np.zeros((self.p,self.p))

for k in range(self.n):
b = b + np.dot(self.Yt[:,k],P@self.Yt[:,k]) + \
2*np.dot(self.Yt[:,k],R@self.Ut[:,k])/self.c[k] + \
np.dot(self.Ut[:,k],self.Vt[:,k])/(self.c[k]**2)
P = P + np.dot(self.Ut[:,k],self.Vt[:,k])*np.outer(self.Zt[:,k],self.Zt[:,k]) + \
np.outer(self.Zt[:,k],R@self.Ut[:,k]) + \
np.outer(R@self.Ut[:,k],self.Zt[:,k])
R = R + np.outer(self.Zt[:,k],self.Vt[:,k])

return b
7 changes: 7 additions & 0 deletions egrssmatrices/spline_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import numpy as np

def spline_kernel(t,p):
T = np.vander(t, 2*p)/np.flip(np.cumprod(np.hstack([1,np.arange(1,2*p)])))
Ut = T[:,p:].T
Vt = (np.fliplr(T[:,:p])*((-1)**np.arange(0,p))).T
return Ut,Vt
21 changes: 21 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[build-system]
requires = ["hatchling", "numpy"]
build-backend = "hatchling.build"

[project]
name = "egrssmatrices"
version = "0.0.1"
authors = [
{ name="Mikkel Paltorp", email="mikkel.paltorp@gmail.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]

[project.urls]
"Homepage" = "https://github.com/mipals/egrssmatrices"
Empty file added tests/__init__.py
Empty file.
107 changes: 107 additions & 0 deletions tests/test_egrssmatrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import unittest
import numpy as np
import numpy.testing as npt
from egrssmatrices.egrssmatrix import egrssmatrix
from egrssmatrices.spline_kernel import spline_kernel

class testegrssmatrices(unittest.TestCase):

def test_matmul(self):
# Setting up the system
p,n = 2,50
t = np.linspace(0.02,1.0,n)
Ut,Vt = spline_kernel(t,p)
M = egrssmatrix(Ut,Vt)
Md = egrssmatrix(Ut,Vt,t) # Vector diagonal
Ms = egrssmatrix(Ut,Vt,1) # Scalar diagonal
Mfull = M.full()
Mdfull = Md.full()
Msfull = Ms.full()
# Testing Multiplcation
x = np.ones(n)
y = M@x
yd = Md@x
ys = Ms@x
yfull = Mfull@x
ydfull = Mdfull@x
ysfull = Msfull@x
npt.assert_almost_equal(y,yfull)
npt.assert_almost_equal(yd,ydfull)
npt.assert_almost_equal(ys,ysfull)
# Testing __getitem__
npt.assert_equal(Mfull[1,10], M[1,10])
npt.assert_equal(Mfull[10,10], M[10,10])
npt.assert_equal(Mfull[10,1], M[10,1])
npt.assert_equal(Mdfull[1,10], Md[1,10])
npt.assert_equal(Mdfull[10,10],Md[10,10])
npt.assert_equal(Mdfull[10,1], Md[10,1])

def test_cholesky(self):
# Setting up the system
p,n = 2,50
t = np.linspace(0.02,1.0,n)
Ut,Vt = spline_kernel(t,p)
M = egrssmatrix(Ut,Vt)
Md = egrssmatrix(Ut,Vt,t)
Ms = egrssmatrix(Ut,Vt,1)
Mfull = M.full()
Mdfull = Md.full()
Msfull = Ms.full()
# Setting up right-hand sides
x = np.ones(n)
y = M@x
yd = Md@x
ys = Ms@x
# Solving the linear systems
b = M.solve(y)
bd = Md.solve(yd)
bs = Ms.solve(ys)
bfull = np.linalg.solve(Mfull,y)
bdfull = np.linalg.solve(Mdfull,yd)
bsfull = np.linalg.solve(Msfull,ys)
npt.assert_almost_equal(b,bfull)
npt.assert_almost_equal(bd,bdfull)
npt.assert_almost_equal(bs,bsfull)
# Testing traces
dtracefull = np.matrix.trace(np.linalg.solve(Mdfull,Mfull))
stracefull = np.matrix.trace(np.linalg.solve(Msfull,Mfull))
dtrace = Md.trace_inverse_product()
strace = Ms.trace_inverse_product()
npt.assert_almost_equal(dtrace,dtracefull)
npt.assert_almost_equal(strace,stracefull)

def test_cholesky_inverse(self):
# Setting up the system
p,n = 2,50
t = np.linspace(0.02,1.0,n)
Ut,Vt = spline_kernel(t,p)
Md = egrssmatrix(Ut,Vt,t)
Ms = egrssmatrix(Ut,Vt,1)
Mdfull = Md.full()
Msfull = Ms.full()
# Computing the inverse of the Cholesky factor
Md.cholesky_inverse()
Ms.cholesky_inverse()
Tdinv = np.tril(Md.Yt.T@Md.Zt,-1) + np.diag(1/Md.c)
Tsinv = np.tril(Ms.Yt.T@Ms.Zt,-1) + np.diag(1/Ms.c)
npt.assert_almost_equal(Tdinv,np.linalg.inv(np.linalg.cholesky(Mdfull)))
npt.assert_almost_equal(Tsinv,np.linalg.inv(np.linalg.cholesky(Msfull)))

def test_logdet(self):
# Setting up the system
p,n = 2,50
t = np.linspace(0.02,1.0,n)
Ut,Vt = spline_kernel(t,p)
M = egrssmatrix(Ut,Vt)
Md = egrssmatrix(Ut,Vt,t)
Ms = egrssmatrix(Ut,Vt,1)
Mfull = M.full()
Mdfull = Md.full()
Msfull = Ms.full()
# Computing the log-determinant
npt.assert_almost_equal(M.logdet(),np.linalg.slogdet(Mfull)[1])
npt.assert_almost_equal(Md.logdet(),np.linalg.slogdet(Mdfull)[1])
npt.assert_almost_equal(Ms.logdet(),np.linalg.slogdet(Msfull)[1])

if __name__ == '__main__':
unittest.main()

0 comments on commit 9ab052f

Please sign in to comment.