drvarma 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
drvarma/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """drvarma — multivariate VARMA modelling (Python port).
2
+
3
+ Free software under the GNU General Public License v2 or later (see COPYING).
4
+ Python port of the drvarma C engine; see docs/MIGRATION_PLAN.md.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from .series import MultiSeries
10
+ from .inp import load, save, InpSpec
11
+ from .model import Model
12
+ from . import (transform, forecast, diagnostics, irf, deseason, datasets,
13
+ report, report_forecast, elfvarma_py, estimate_py, plots,
14
+ volatility)
15
+
16
+ __all__ = ["MultiSeries", "load", "save", "InpSpec", "Model",
17
+ "transform", "forecast", "diagnostics", "irf", "deseason",
18
+ "datasets", "report", "report_forecast", "elfvarma_py",
19
+ "estimate_py", "plots", "volatility", "__version__"]
drvarma/_as311.py ADDED
@@ -0,0 +1,531 @@
1
+ """Faithful Python port of Mauricio's AS 311 exact VARMA log-likelihood.
2
+
3
+ Direct translation of ``csrc/internal/elfvarma.c`` (functions ``elf``, ``cgamma``,
4
+ ``cxi``, ``cres``, ``chekma``) by J.A. Mauricio — *not* a Kalman/state-space
5
+ reimplementation. Mauricio (1995) JASA 90, 282-291; (1997) Applied Statistics
6
+ 46, 157-171 [AS 311].
7
+
8
+ The C is 1-indexed (Numerical-Recipes style); to keep the index arithmetic an
9
+ exact transcription, the arrays here are also 1-indexed — allocated with a
10
+ leading unused slot and addressed from 1. Linear-algebra primitives (Cholesky,
11
+ forward substitution, quadratic forms) use NumPy, matching the C ``choldcp`` /
12
+ ``cholfor`` / ``cholbak`` which compute the same factorisations.
13
+
14
+ License: GPL-2.0-or-later
15
+ """
16
+
17
+ import numpy as np
18
+
19
+ _LOG2PI = 1.837877066
20
+
21
+
22
+ # --------------------------------------------------------------------------- #
23
+ # 1-indexed helpers #
24
+ # --------------------------------------------------------------------------- #
25
+
26
+ def _m1(r, c):
27
+ return np.zeros((r + 1, c + 1))
28
+
29
+
30
+ def _to_1based_cube(arr, p, m):
31
+ """(p, m, m) 0-indexed -> (p+1, m+1, m+1) 1-indexed cube."""
32
+ out = np.zeros((p + 1, m + 1, m + 1))
33
+ for k in range(p):
34
+ out[k + 1, 1:, 1:] = arr[k]
35
+ return out
36
+
37
+
38
+ def _chol_lower(A_1, n):
39
+ """Cholesky lower factor of a 1-indexed symmetric PD matrix A_1 (n x n).
40
+
41
+ Returns (L_1, detfac, ifault): L_1 is 1-indexed lower-triangular with
42
+ A = L L'; detfac = det(A); ifault=1 if not positive definite.
43
+ """
44
+ A = A_1[1:n + 1, 1:n + 1]
45
+ A = 0.5 * (A + A.T)
46
+ try:
47
+ L = np.linalg.cholesky(A)
48
+ except np.linalg.LinAlgError:
49
+ return None, 0.0, 1
50
+ L1 = np.zeros((n + 1, n + 1))
51
+ L1[1:n + 1, 1:n + 1] = L
52
+ detfac = float(np.prod(np.diag(L)) ** 2)
53
+ return L1, detfac, 0
54
+
55
+
56
+ # --------------------------------------------------------------------------- #
57
+ # chekma : MA invertibility via companion eigenvalues #
58
+ # --------------------------------------------------------------------------- #
59
+
60
+ def chekma(m, q, Theta):
61
+ """Return ifault=1 if the MA operator is (near) non-invertible.
62
+
63
+ Theta is 1-indexed (q+1, m+1, m+1); mirrors elfvarma.c:chekma.
64
+ """
65
+ if q == 0:
66
+ return 0
67
+ n = m * q
68
+ A = np.zeros((n, n))
69
+ for k in range(1, q + 1):
70
+ for i in range(1, m + 1):
71
+ for j in range(1, m + 1):
72
+ A[i - 1, j - 1 + (k - 1) * m] = Theta[k][i][j]
73
+ for k in range(1, q):
74
+ for j in range(1, m + 1):
75
+ A[j - 1 + k * m, j - 1 + (k - 1) * m] = 1.0
76
+ wmod = np.abs(np.linalg.eigvals(A))
77
+ return 1 if np.any(wmod >= 1.00005) else 0
78
+
79
+
80
+ # --------------------------------------------------------------------------- #
81
+ # cgamma : autocovariances Gamma(k) and cross-covariances Gamma_wa(k) #
82
+ # --------------------------------------------------------------------------- #
83
+
84
+ def cgamma(m, p, q, Phi, Theta, Qq):
85
+ """Port of elfvarma.c:cgamma.
86
+
87
+ Returns (gamma, gamwa, ifault):
88
+ gamma : 1-indexed packed vector, length big = m(m+1)/2 + m^2 (p-1)
89
+ gamwa : dict {k: (m+1,m+1) 1-indexed} for k = -q+1 .. 0
90
+ ifault: 1 if the Yule-Walker system is singular (AR unit root)
91
+ """
92
+ gamwa = {k: _m1(m, m) for k in range(-q + 1, 1)}
93
+
94
+ # [1]: cross-covariance matrices
95
+ if q > 0:
96
+ for i in range(1, m + 1):
97
+ for j in range(1, m + 1):
98
+ gamwa[0][i][j] = Qq[i][j]
99
+ for k in range(1, q): # k = 1 .. q-1
100
+ for i in range(1, m + 1):
101
+ for j in range(1, m + 1):
102
+ s = 0.0
103
+ for h in range(1, m + 1):
104
+ s -= Theta[k][i][h] * Qq[h][j]
105
+ for l in range(1, k + 1):
106
+ if l <= p:
107
+ for h in range(1, m + 1):
108
+ s += Phi[l][i][h] * gamwa[l - k][h][j]
109
+ gamwa[-k][i][j] = s
110
+
111
+ big = m * (m + 1) // 2 + m * m * (p - 1)
112
+ if p == 0:
113
+ return np.zeros(0), gamwa, 0
114
+
115
+ # [2]: w(0)
116
+ wzero = _m1(m, m)
117
+ mzero = _m1(m, m)
118
+ for i in range(1, p + 1):
119
+ for j in range(i, q + 1):
120
+ for ii in range(1, m + 1):
121
+ for jj in range(1, m + 1):
122
+ s = 0.0
123
+ for k in range(1, m + 1):
124
+ s += Phi[i][ii][k] * gamwa[i - j][k][jj]
125
+ mzero[ii][jj] = s
126
+ for ii in range(1, m + 1):
127
+ for jj in range(1, m + 1):
128
+ s = 0.0
129
+ for k in range(1, m + 1):
130
+ s += mzero[ii][k] * Theta[j][jj][k]
131
+ wzero[ii][jj] += s
132
+ for i in range(1, m + 1):
133
+ for j in range(i, m + 1):
134
+ wzero[i][j] = Qq[i][j] - wzero[i][j] - wzero[j][i]
135
+ for j in range(1, q + 1):
136
+ for ii in range(1, m + 1):
137
+ for jj in range(1, m + 1):
138
+ s = 0.0
139
+ for k in range(1, m + 1):
140
+ s += Theta[j][ii][k] * Qq[k][jj]
141
+ mzero[ii][jj] = s
142
+ for ii in range(1, m + 1):
143
+ for jj in range(ii, m + 1):
144
+ s = 0.0
145
+ for k in range(1, m + 1):
146
+ s += mzero[ii][k] * Theta[j][jj][k]
147
+ wzero[ii][jj] += s
148
+
149
+ # [3]: linear system
150
+ mat = _m1(big, big)
151
+ rhs = np.zeros(big + 1)
152
+
153
+ # [3.1] first m(m+1)/2 rows
154
+ for j in range(1, m + 1):
155
+ for i in range(1, j + 1):
156
+ row = j * (j - 1) // 2 + i
157
+ for l in range(1, m + 1):
158
+ for k in range(1, l + 1):
159
+ col = l * (l - 1) // 2 + k
160
+ s = 0.0
161
+ if k == l:
162
+ for r in range(1, p + 1):
163
+ s -= Phi[r][i][k] * Phi[r][j][l]
164
+ else:
165
+ for r in range(1, p + 1):
166
+ s -= (Phi[r][i][k] * Phi[r][j][l]
167
+ + Phi[r][i][l] * Phi[r][j][k])
168
+ mat[row][col] = s
169
+ for sv in range(1, p):
170
+ for l in range(1, m + 1):
171
+ for k in range(1, m + 1):
172
+ col = m * (m + 1) // 2 + m * m * (sv - 1) + m * (l - 1) + k
173
+ s = 0.0
174
+ for r in range(1, p - sv + 1):
175
+ s -= (Phi[r + sv][i][k] * Phi[r][j][l]
176
+ + Phi[r + sv][j][k] * Phi[r][i][l])
177
+ mat[row][col] = s
178
+ mat[row][row] += 1.0
179
+ rhs[row] = wzero[i][j]
180
+
181
+ # [3.2] remaining m^2 (p-1) rows
182
+ for sv in range(1, p):
183
+ for i in range(1, m + 1):
184
+ for j in range(1, m + 1):
185
+ row = m * (m + 1) // 2 + m * m * (sv - 1) + m * (i - 1) + j
186
+ for l in range(1, m + 1):
187
+ if l <= j:
188
+ col = j * (j - 1) // 2 + l
189
+ else:
190
+ col = l * (l - 1) // 2 + j
191
+ mat[row][col] = -Phi[sv][i][l]
192
+ for r in range(1, p):
193
+ for l in range(1, m + 1):
194
+ col = m * (m + 1) // 2 + m * m * (r - 1) + m * (j - 1) + l
195
+ if r + sv <= p:
196
+ mat[row][col] = -Phi[r + sv][i][l]
197
+ if sv > r:
198
+ col2 = m * (m + 1) // 2 + m * m * (r - 1) + m * (l - 1) + j
199
+ mat[row][col2] -= Phi[sv - r][i][l]
200
+ mat[row][row] += 1.0
201
+ val = 0.0
202
+ for h in range(sv, q + 1):
203
+ for k in range(1, m + 1):
204
+ val -= gamwa[sv - h][j][k] * Theta[h][i][k]
205
+ rhs[row] = val
206
+
207
+ # [4]: solve
208
+ try:
209
+ sol = np.linalg.solve(mat[1:big + 1, 1:big + 1], rhs[1:big + 1])
210
+ except np.linalg.LinAlgError:
211
+ return np.zeros(big + 1), gamwa, 1
212
+ gamma = np.zeros(big + 1)
213
+ gamma[1:big + 1] = sol
214
+ return gamma, gamwa, 0
215
+
216
+
217
+ # --------------------------------------------------------------------------- #
218
+ # cxi : Green's-function matrix sequence Xi_k, premultiplied by q1inv #
219
+ # --------------------------------------------------------------------------- #
220
+
221
+ def cxi(m, n, q, Theta, Q1inv, xitol):
222
+ """Port of elfvarma.c:cxi. Returns (nlim, rxi) with rxi[k] (k=0..n-1)
223
+ a (m+1,m+1) 1-indexed matrix; rxi premultiplied by the lower-triangular q1inv.
224
+ """
225
+ rxi = np.zeros((n, m + 1, m + 1))
226
+ for i in range(1, m + 1):
227
+ rxi[0][i][i] = 1.0
228
+
229
+ r = 0
230
+ delta = False
231
+ while True:
232
+ r += 1
233
+ for jx in range(1, q + 1):
234
+ if r >= jx:
235
+ for ii in range(1, m + 1):
236
+ for jj in range(1, m + 1):
237
+ s1 = 0.0
238
+ for h in range(1, m + 1):
239
+ s1 += Theta[jx][ii][h] * rxi[r - jx][h][jj]
240
+ rxi[r][ii][jj] += s1
241
+ s2 = float(np.sum(np.abs(rxi[r])))
242
+ if s2 < xitol:
243
+ nq = 1
244
+ delta = True
245
+ while (nq <= q) and (r < n - 1) and delta:
246
+ nq += 1
247
+ r += 1
248
+ for jx in range(1, q + 1):
249
+ if r >= jx:
250
+ for ii in range(1, m + 1):
251
+ for jj in range(1, m + 1):
252
+ s1 = 0.0
253
+ for h in range(1, m + 1):
254
+ s1 += Theta[jx][ii][h] * rxi[r - jx][h][jj]
255
+ rxi[r][ii][jj] += s1
256
+ s2 = float(np.sum(np.abs(rxi[r])))
257
+ if s2 > xitol:
258
+ delta = False
259
+ if delta:
260
+ r -= nq
261
+ if delta or r >= n - 1:
262
+ break
263
+ nlim = r
264
+
265
+ # [2]: premultiply each rxi[k] by q1inv (lower triangular)
266
+ for k in range(0, nlim + 1):
267
+ mtmp = _m1(m, m)
268
+ for i in range(1, m + 1):
269
+ for jx in range(1, m + 1):
270
+ s1 = 0.0
271
+ for h in range(1, i + 1):
272
+ s1 += Q1inv[i][h] * rxi[k][h][jx]
273
+ mtmp[i][jx] = s1
274
+ rxi[k][1:, 1:] = mtmp[1:, 1:]
275
+ return nlim, rxi
276
+
277
+
278
+ # --------------------------------------------------------------------------- #
279
+ # cres : exact residuals #
280
+ # --------------------------------------------------------------------------- #
281
+
282
+ def cres(m, n, g, nlim, rxi, Q1, M, L, lam, res):
283
+ """Port of elfvarma.c:cres. Overwrites res (1-indexed (n+1, m+1))."""
284
+ mg = m * g
285
+ # [1] solve L' c = lambda (L lower 1-indexed) -> overwrite lam[1..mg]
286
+ Lmat = L[1:mg + 1, 1:mg + 1]
287
+ from scipy.linalg import solve_triangular
288
+ c = solve_triangular(Lmat, lam[1:mg + 1], lower=True, trans='T')
289
+ lam[1:mg + 1] = c
290
+ # [2] d = M c (M lower 1-indexed)
291
+ for i in range(mg, 0, -1):
292
+ s = 0.0
293
+ for j in range(1, i + 1):
294
+ s += M[i][j] * lam[j]
295
+ lam[i] = s
296
+ # [3] residual correction
297
+ for i in range(1, n + 1):
298
+ for j in range(1, i + 1):
299
+ if (i - j <= nlim) and (j <= g):
300
+ for jj in range(1, m + 1):
301
+ s = 0.0
302
+ for h in range(1, m + 1):
303
+ s += rxi[i - j][jj][h] * lam[h + (j - 1) * m]
304
+ res[i][jj] -= s
305
+ for j in range(1, n + 1):
306
+ for i in range(m, 0, -1):
307
+ s = 0.0
308
+ for h in range(1, i + 1):
309
+ s += Q1[i][h] * res[j][h]
310
+ res[j][i] = s
311
+
312
+
313
+ # --------------------------------------------------------------------------- #
314
+ # elf : the exact log-likelihood #
315
+ # --------------------------------------------------------------------------- #
316
+
317
+ def elf(m, n, p, q, Mu, Phi, Theta, Qq, W, sigma2, xitol, atf):
318
+ """Port of elfvarma.c:elf.
319
+
320
+ All matrix arguments are 1-indexed (Mu (m+1,), Phi/Theta cubes, Qq (m+1,m+1),
321
+ W (n+1, m+1)). Returns (logelf, f1, f2, a, ifault) where a is the 1-indexed
322
+ residual matrix (n+1, m+1) (filled only if atf=True).
323
+ """
324
+ g = max(p, q)
325
+ a = np.zeros((n + 1, m + 1))
326
+
327
+ # [0]/[1]: q1 = chol(Qq), q1inv, detq
328
+ Q1full = _m1(m, m)
329
+ for i in range(1, m + 1):
330
+ for j in range(i, m + 1):
331
+ Q1full[i][j] = Qq[i][j]
332
+ Q1full[j][i] = Qq[i][j]
333
+ Q1, detq, ifault = _chol_lower(Q1full, m)
334
+ if ifault:
335
+ return 0.0, 0.0, 0.0, a, 1
336
+ from scipy.linalg import solve_triangular
337
+ Lq = Q1[1:m + 1, 1:m + 1]
338
+ Q1inv_full = solve_triangular(Lq, np.eye(m), lower=True) # inv of lower chol
339
+ Q1inv = _m1(m, m)
340
+ Q1inv[1:m + 1, 1:m + 1] = Q1inv_full
341
+
342
+ # [2]: autocovariances
343
+ gamma = np.zeros(0)
344
+ gamwa = {}
345
+ if p > 0:
346
+ gamma, gamwa, ifg = cgamma(m, p, q, Phi, Theta, Qq)
347
+ if ifg:
348
+ return 0.0, 0.0, 0.0, a, 2
349
+
350
+ half = m * (m + 1) // 2
351
+
352
+ def gval(k, ii, kk):
353
+ """gamma packing access used in [3.1] (see elfvarma.c)."""
354
+ if k > 0:
355
+ jl = half + m * m * (k - 1) + m * (kk - 1) + ii
356
+ elif k < 0:
357
+ jl = half - m * m * (k + 1) + m * (ii - 1) + kk
358
+ else:
359
+ if kk >= ii:
360
+ jl = kk * (kk - 1) // 2 + ii
361
+ else:
362
+ jl = ii * (ii - 1) // 2 + kk
363
+ return gamma[jl]
364
+
365
+ # [3]: M = chol(v1 omega v1')
366
+ mg = m * g
367
+ mtmp0 = _m1(mg, mg)
368
+
369
+ # [3.1]: omega * v1' -> mtmp1 ((p+q)m x g m)
370
+ mtmp1 = _m1(m * (p + q), mg)
371
+ for i in range(1, p + 1):
372
+ for j in range(1, g + 1):
373
+ for k in range(j - i, p - i + 1):
374
+ for ii in range(1, m + 1):
375
+ for jj in range(1, m + 1):
376
+ s1 = 0.0
377
+ for kk in range(1, m + 1):
378
+ s1 += gval(k, ii, kk) * Phi[p - k - i + j][jj][kk]
379
+ mtmp1[ii + (i - 1) * m][jj + (j - 1) * m] += s1
380
+ for k in range(j - i, q - i + 1):
381
+ if p + k <= q:
382
+ for ii in range(1, m + 1):
383
+ for jj in range(1, m + 1):
384
+ s1 = 0.0
385
+ for kk in range(1, m + 1):
386
+ s1 += gamwa[-q + p + k][ii][kk] * Theta[q - k - i + j][jj][kk]
387
+ mtmp1[ii + (i - 1) * m][jj + (j - 1) * m] -= s1
388
+ for i in range(p + 1, p + q + 1):
389
+ for j in range(1, g + 1):
390
+ for k in range(p + j - i, p + p - i + 1):
391
+ if p - k <= q:
392
+ for ii in range(1, m + 1):
393
+ for jj in range(1, m + 1):
394
+ s1 = 0.0
395
+ for kk in range(1, m + 1):
396
+ s1 += gamwa[-q + p - k][kk][ii] * Phi[p + p - k - i + j][jj][kk]
397
+ mtmp1[ii + (i - 1) * m][jj + (j - 1) * m] += s1
398
+ if p - i + j <= 0:
399
+ for ii in range(1, m + 1):
400
+ for jj in range(1, m + 1):
401
+ s1 = 0.0
402
+ for kk in range(1, m + 1):
403
+ s1 += Qq[ii][kk] * Theta[q + p - i + j][jj][kk]
404
+ mtmp1[ii + (i - 1) * m][jj + (j - 1) * m] -= s1
405
+
406
+ # [3.2]: v1 omega v1' -> mtmp0 (upper triangle)
407
+ for i in range(1, g + 1):
408
+ for j in range(i, g + 1):
409
+ for k in range(0, p - i + 1):
410
+ for ii in range(1, m + 1):
411
+ jl = ii if i == j else 1
412
+ for jj in range(jl, m + 1):
413
+ s1 = 0.0
414
+ for kk in range(1, m + 1):
415
+ s1 += Phi[p - k][ii][kk] * mtmp1[kk + (k + i - 1) * m][jj + (j - 1) * m]
416
+ mtmp0[ii + (i - 1) * m][jj + (j - 1) * m] += s1
417
+ for k in range(0, q - i + 1):
418
+ for ii in range(1, m + 1):
419
+ jl = ii if i == j else 1
420
+ for jj in range(jl, m + 1):
421
+ s1 = 0.0
422
+ for kk in range(1, m + 1):
423
+ s1 += Theta[q - k][ii][kk] * mtmp1[kk + (k + p + i - 1) * m][jj + (j - 1) * m]
424
+ mtmp0[ii + (i - 1) * m][jj + (j - 1) * m] -= s1
425
+
426
+ # symmetrise mtmp0 (it was filled on/above the block-diagonal upper triangle)
427
+ Vfull = mtmp0[1:mg + 1, 1:mg + 1].copy()
428
+ Vfull = np.triu(Vfull) + np.triu(Vfull, 1).T
429
+ mtmp0[1:mg + 1, 1:mg + 1] = Vfull
430
+
431
+ # [3.3]: M = chol(mtmp0)
432
+ M, _detM, ifault = _chol_lower(mtmp0, mg)
433
+ if ifault:
434
+ return 0.0, 0.0, 0.0, a, 3
435
+
436
+ # [4.1]: MA invertibility
437
+ if q > 0 and chekma(m, q, Theta):
438
+ return 0.0, 0.0, 0.0, a, 4
439
+
440
+ # [4.2]: rxi
441
+ nlim, rxi = cxi(m, n, q, Theta, Q1inv, xitol)
442
+
443
+ # [5]: eta -> a (conditional residuals, then premultiply by q1inv).
444
+ # Vectorised equivalent of the C double loop: AR part by shifted matmuls,
445
+ # MA part by the (inherently sequential) feedback recursion.
446
+ x0 = W[1:n + 1, 1:m + 1] - Mu[1:m + 1] # (n, m), 0-indexed
447
+ phi0 = Phi[1:p + 1, 1:m + 1, 1:m + 1] if p else np.zeros((0, m, m))
448
+ theta0 = Theta[1:q + 1, 1:m + 1, 1:m + 1] if q else np.zeros((0, m, m))
449
+ b = x0.copy()
450
+ for jx in range(1, p + 1):
451
+ b[jx:] -= x0[:n - jx] @ phi0[jx - 1].T # - Phi_j x_{t-j}
452
+ a0 = b
453
+ if q:
454
+ a0 = b.copy()
455
+ for t in range(n):
456
+ acc = b[t]
457
+ for jx in range(1, q + 1):
458
+ if t - jx >= 0:
459
+ acc = acc + theta0[jx - 1] @ a0[t - jx]
460
+ a0[t] = acc
461
+ a0 = a0 @ Q1inv[1:m + 1, 1:m + 1].T # [5.2] premultiply by q1inv
462
+ a[1:n + 1, 1:m + 1] = a0
463
+
464
+ # [6]: M' h -> vechh. h_j = sum_i Xi_i' a_{i+j} (i = 0..min(nlim, n-j)).
465
+ vechh = np.zeros(mg + 1)
466
+ R = rxi[:nlim + 1, 1:m + 1, 1:m + 1] # (nlim+1, m, m)
467
+ a0 = a[1:n + 1, 1:m + 1] # (n, m), 0-indexed rows
468
+ for j in range(1, g + 1):
469
+ kmax = min(nlim, n - j)
470
+ if kmax >= 0:
471
+ # sum_i Xi_i^T a_{i+j} ; a_{i+j} (1-indexed) = a0[i+j-1]
472
+ Hj = np.einsum('iab,ia->b', R[:kmax + 1], a0[j - 1:j + kmax])
473
+ vechh[1 + (j - 1) * m:j * m + 1] = Hj
474
+ # premultiply by M' : vechh = M^T vechh
475
+ Mmat = M[1:mg + 1, 1:mg + 1]
476
+ vechh[1:mg + 1] = Mmat.T @ vechh[1:mg + 1]
477
+
478
+ Msave = M.copy() if atf else None
479
+
480
+ # [7]: H'H -> mtmp2. First block-column (i,1) = sum_k Xi_k' Xi_{k+i-1};
481
+ # remaining lower blocks by the recursion (i,j) = (i-1,j-1) - corr.
482
+ rx = rxi[:, 1:m + 1, 1:m + 1] # (n, m, m), 0-indexed
483
+ blk = np.zeros((g + 1, g + 1, m, m)) # blk[i][j] = HtH block (i,j)
484
+ for i in range(1, g + 1):
485
+ kmax = min(n - i, nlim - i + 1)
486
+ if kmax >= 0:
487
+ blk[i][1] = np.einsum('kab,kac->bc', rx[:kmax + 1], rx[i - 1:i + kmax])
488
+ for i in range(2, g + 1):
489
+ for j in range(2, i + 1):
490
+ corr = np.zeros((m, m))
491
+ if (n - i + 1 <= nlim) and (n - j + 1 <= nlim):
492
+ corr = rx[n - i + 1].T @ rx[n - j + 1]
493
+ blk[i][j] = blk[i - 1][j - 1] - corr
494
+ HtH = np.zeros((mg, mg))
495
+ for i in range(1, g + 1):
496
+ for j in range(1, i + 1):
497
+ HtH[(i - 1) * m:i * m, (j - 1) * m:j * m] = blk[i][j]
498
+ HtH = np.tril(HtH) + np.tril(HtH, -1).T # symmetrise from lower
499
+
500
+ # [8]: I + M'H'HM and its Cholesky L
501
+ ImMtHtHM = np.eye(mg) + (Mmat.T @ HtH) @ Mmat
502
+ try:
503
+ Lom = np.linalg.cholesky(0.5 * (ImMtHtHM + ImMtHtHM.T))
504
+ except np.linalg.LinAlgError:
505
+ return 0.0, 0.0, 0.0, a, 5
506
+ detom = float(np.prod(np.diag(Lom)) ** 2)
507
+
508
+ # [9]: lambda via forward substitution L lambda = vechh
509
+ lam_vec = solve_triangular(Lom, vechh[1:mg + 1], lower=True)
510
+ vechh[1:mg + 1] = lam_vec
511
+
512
+ # [10]: quadratic form f1
513
+ s1 = float(np.sum(a[1:n + 1, 1:m + 1] ** 2))
514
+ s2 = float(np.sum(vechh[1:mg + 1] ** 2))
515
+ f1 = s1 - s2
516
+
517
+ # [11]: determinant factor
518
+ f2 = detq * np.exp(np.log(detom) / n)
519
+
520
+ # [12]: log-likelihood
521
+ logelf = -0.5 * (n * m * (_LOG2PI + np.log(sigma2)) + n * np.log(detq)
522
+ + np.log(detom) + f1 / sigma2)
523
+
524
+ # [13]: residuals
525
+ if atf:
526
+ # rebuild L as 1-indexed for cres
527
+ L1 = _m1(mg, mg)
528
+ L1[1:mg + 1, 1:mg + 1] = Lom
529
+ cres(m, n, g, nlim, rxi, Q1, Msave, L1, vechh, a)
530
+
531
+ return float(logelf), float(f1), float(f2), a, 0