Coverage for src/lsqfitgp/_linalg/_toeplitz.py: 96%

136 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-04 11:43 +0000

1# lsqfitgp/_linalg/_toeplitz.py 

2# 

3# Copyright (c) 2020, 2022, 2023, 2026, Giacomo Petrillo 

4# 

5# This file is part of lsqfitgp. 

6# 

7# lsqfitgp is free software: you can redistribute it and/or modify 

8# it under the terms of the GNU General Public License as published by 

9# the Free Software Foundation, either version 3 of the License, or 

10# (at your option) any later version. 

11# 

12# lsqfitgp is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the GNU General Public License 

18# along with lsqfitgp. If not, see <http://www.gnu.org/licenses/>. 

19 

20import numpy 1bca

21from jax import jit 1bca

22from jax import numpy as jnp 1bca

23 

24from lsqfitgp._linalg import _seqalg 1bca

25 

26 

27class SymSchur(_seqalg.Producer): 1bca

28 """ 

29 Cholesky decomposition of a symmetric Toeplitz matrix. 

30 

31 Adapted from TOEPLITZ_CHOLESKY by John Burkardt (LGPL license) 

32 http://people.sc.fsu.edu/~jburkardt/py_src/toeplitz_cholesky/toeplitz_cholesky.html 

33 """ 

34 

35 def __init__(self, t): 1bca

36 """`t` = first row of the matrix.""" 

37 t = jnp.asarray(t) 1a

38 assert t.ndim == 1 1a

39 # assert t[0] > 0, '1-th leading minor is not positive definite' 

40 self.t = t 1a

41 

42 inputs = () 1bca

43 

44 def init(self, n): 1bca

45 t = self.t 1a

46 del self.t 1a

47 assert len(t) == n 1a

48 norm = t[0] 1a

49 t = t / norm 1a

50 self.g = jnp.stack([t, t]) 1a

51 self.snorm = jnp.sqrt(norm) 1a

52 

53 def iter_out(self, i): # noqa: ARG002, interface method 1bca

54 """`i`-th column of Cholesky factor L.""" 

55 return self.g[0, :] * self.snorm 1a

56 

57 def iter(self, i): 1bca

58 g = self.g 1a

59 g = g.at[0, :].set(jnp.roll(g[0, :], 1)) 1a

60 g = g.at[:, 0].set(0).at[:, i - 1].set(0) 1a

61 # assert g[0, i] > 0, 'what??' 

62 rho = -g[1, i] / g[0, i] 1a

63 # assert abs(rho) < 1, f'{i+1}-th leading minor is not positive definite' 

64 gamma = jnp.sqrt((1 - rho) * (1 + rho)) 1a

65 self.g = (g + g[::-1] * rho) / gamma 1a

66 

67 

68class SymLevinson(_seqalg.Producer): 1bca

69 """ 

70 Cholesky decomposition of the *inverse* of a symmetric Toeplitz matrix. 

71 

72 Adapted form SuperGauss/DurbinLevinson.h (GPL license) 

73 https://cran.r-project.org/package=SuperGauss 

74 

75 Note: Schur should be more accurate than Levinson 

76 """ 

77 

78 def __init__(self, t): 1bca

79 """`t` = first row of the matrix.""" 

80 t = jnp.asarray(t, float) 1ba

81 assert t.ndim == 1 1ba

82 # assert t[0] > 0, '1-th leading minor is not positive definite' 

83 self.t = t 1ba

84 

85 inputs = () 1bca

86 

87 def init(self, n): 1bca

88 self.phi1 = jnp.zeros(n) 1ba

89 self.phi2 = jnp.zeros(n) 1ba

90 self.nu = self.t[0] 1ba

91 self.tlag = jnp.roll(self.t, -1) 1ba

92 del self.t 1ba

93 

94 def iter_out(self, i): 1bca

95 """`i`-th row of L^-1.""" 

96 return -self.phi2.at[i].set(-1) / jnp.sqrt(self.nu) 1ba

97 

98 def iter(self, i): 1bca

99 phi1 = self.phi1 1ba

100 phi2 = self.phi2 1ba

101 nu = self.nu 1ba

102 tlag = self.tlag 1ba

103 

104 pi = i - 1 1ba

105 rp = phi2 @ tlag 1ba

106 phi1 = phi1.at[pi].set((tlag[pi] - rp) / nu) 1ba

107 phi1 = phi1 - phi1[pi] * phi2 1ba

108 # assert abs(phi1[pi]) < 1, f'{i+1}-th leading minor is not positive definite' 

109 nu = nu * (1 - phi1[pi]) * (1 + phi1[pi]) 1ba

110 phi2 = jnp.roll(phi1[::-1], i) 1ba

111 

112 self.phi1 = phi1 1ba

113 self.phi2 = phi2 1ba

114 self.nu = nu 1ba

115 

116 

117@jit 1bca

118def chol(t): 1bca

119 _, out = _seqalg.sequential_algorithm(len(t), [SymSchur(t), _seqalg.Stack(0)]) 1a

120 return out.T 1a

121 

122 

123@jit 1bca

124def chol_solve(t, *bs): 1bca

125 ops = [SymSchur(t)] + [_seqalg.SolveTriLowerColByFull(0, b) for b in bs] 1a

126 out = _seqalg.sequential_algorithm(len(t), ops) 1a

127 return out[1] if len(bs) == 1 else out[1:] 1a

128 

129 

130@jit 1bca

131def chol_matmul(t, b): 1bca

132 ops = [SymSchur(t), _seqalg.Rows(b), _seqalg.MatMulColByRow(0, 1)] 1a

133 _, _, out = _seqalg.sequential_algorithm(len(t), ops) 1a

134 return out 1a

135 

136 

137@jit 1bca

138def chol_transp_matmul(t, b): 1bca

139 ops = [SymSchur(t), _seqalg.MatMulRowByFull(0, b), _seqalg.Stack(1)] 

140 _, _, out = _seqalg.sequential_algorithm(len(t), ops) 

141 return out 

142 

143 

144@jit 1bca

145def logdet(t): 1bca

146 _, out = _seqalg.sequential_algorithm(len(t), [SymSchur(t), _seqalg.SumLogDiag(0)]) 1a

147 return 2 * out 1a

148 

149 

150@jit 1bca

151def solve(t, b): 1bca

152 ops = [SymLevinson(t), _seqalg.MatMulRowByFull(0, b), _seqalg.MatMulColByRow(0, 1)] 1ba

153 _, _, out = _seqalg.sequential_algorithm(len(t), ops) 1ba

154 return out 1ba

155 

156 

157@jit 1bca

158def chol_transp_solve(t, b): 1bca

159 ops = [SymLevinson(t), _seqalg.Rows(b), _seqalg.MatMulColByRow(0, 1)] 

160 _, _, out = _seqalg.sequential_algorithm(len(t), ops) 

161 return out 

162 

163 

164def chol_solve_numpy(t, b, diageps=None): 1bca

165 """ 

166 Solve a linear system for the cholesky factor of a symmetric Toeplitz matrix. 

167 

168 The algorithm is: 

169 

170 t[0] += diageps 

171 m = toeplitz(t) 

172 l = chol(m) 

173 return solve(l, b) 

174 

175 Numpy object arrays are supported. Broadcasts like matmul. 

176 

177 Parameters 

178 ---------- 

179 t : (..., n) array 

180 The first row or column of the matrix. 

181 b : (..., n, m) or (n,) array 

182 The right hand side of the linear system. 

183 diageps : scalar, optional 

184 Term added to the diagonal elements of the matrix for regularization. 

185 

186 Returns 

187 ------- 

188 x : (..., n, m) or (..., n) array 

189 The solution ``solve(l, b)``. 

190 """ 

191 t = numpy.array(t, subok=True) 1a

192 n = t.shape[-1] 1a

193 

194 b = numpy.asanyarray(b) 1a

195 vec = b.ndim < 2 1a

196 if vec: 1a

197 b = b[:, None] 1a

198 assert b.shape[-2] == n 1a

199 

200 t = t.astype(numpy.result_type(t, 0.1), copy=False) 1a

201 b = b.astype(numpy.result_type(b, 0.1), copy=False) 1a

202 

203 if n == 0: 1a

204 shape = numpy.broadcast_shapes(t.shape[:-1], b.shape[:-2]) 1a

205 shape += (n,) if vec else b.shape[-2:] 1a

206 dtype = numpy.result_type(t.dtype, b.dtype) 1a

207 return numpy.empty(shape, dtype) 1a

208 

209 if diageps is not None: 1a

210 t[..., 0] += diageps 1a

211 

212 if numpy.any(t[..., 0] <= 0): 1a

213 msg = '1-th leading minor is not positive definite' 1a

214 raise numpy.linalg.LinAlgError(msg) 1a

215 

216 norm = numpy.copy(t[..., 0, None], subok=True) 1a

217 t /= norm 1a

218 invLb = numpy.copy(numpy.broadcast_arrays(b, t[..., None])[0], subok=True) 1a

219 prevLi = t 1a

220 g = numpy.stack([numpy.roll(t, 1, -1), t], -2) 1a

221 

222 for i in range(1, n): 1a

223 assert numpy.all(g[..., 0, i] > 0) 1a

224 rho = -g[..., 1, i, None, None] / g[..., 0, i, None, None] 1a

225 

226 if numpy.any(numpy.abs(rho) >= 1): 1a

227 msg = f'{i + 1}-th leading minor is not positive definite' 1a

228 raise numpy.linalg.LinAlgError(msg) 1a

229 

230 gamma = numpy.sqrt((1 - rho) * (1 + rho)) 1a

231 g[..., :, i:] += g[..., ::-1, i:] * rho 1a

232 g[..., :, i:] /= gamma 1a

233 Li = g[..., 0, i:] # i-th column of L from row i 1a

234 invLb[..., i:, :] -= invLb[..., i - 1, None, :] * prevLi[..., i:, None] 1a

235 invLb[..., i, :] /= Li[..., 0, None] 1a

236 prevLi[..., i:] = Li 1a

237 g[..., 0, i:] = numpy.roll(g[..., 0, i:], 1, -1) 1a

238 

239 invLb /= numpy.sqrt(norm[..., None]) 1a

240 if vec: 1a

241 invLb = numpy.squeeze(invLb, -1) 1a

242 return invLb 1a

243 

244 

245def eigv_bound(t): 1bca

246 """ 

247 Bound the eigenvalues of a symmetric Toeplitz matrix. 

248 

249 Parameters 

250 ---------- 

251 t : array 

252 The first row of the matrix. 

253 

254 Returns 

255 ------- 

256 m : scalar 

257 Any eigenvalue `v` of the matrix satisfies `|v| <= m`. 

258 """ 

259 s = jnp.abs(t) 1a

260 c = jnp.cumsum(s) 1a

261 d = c + c[::-1] - s[0] 1a

262 return jnp.max(d) 1a